lazy-loading.php

I’ll build this as a reusable WordPress theme module, not only basic loading="lazy", but also iframe lazy loading, responsive image helpers, priority control, noscript fallback, exclusions, and admin-safe behavior.

Below is an advanced performance/lazy-loading.php file for your RX Theme. It adds strong image/iframe lazy-loading, LCP/hero image protection, fetchpriority control, decoding optimization, content filtering, block rendering support, iframe optimization, featured image preload, safe exclusions, and helper filters.

WordPress already has modern image-loading optimization using loading, decoding, and fetchpriority; since WordPress 6.3, wp_img_tag_add_loading_optimization_attrs() replaced the older wp_img_tag_add_loading_attr() function, and wp_get_loading_optimization_attributes() is the core helper for loading optimization attributes.

Create this file:

<?php
/**
 * RX Theme - Advanced Lazy Loading & Media Performance
 *
 * File: /performance/lazy-loading.php
 *
 * Purpose:
 * - Improve image, iframe, embed, and media loading performance.
 * - Protect above-the-fold/LCP images from bad lazy loading.
 * - Add loading="lazy", decoding="async", fetchpriority controls.
 * - Preload featured/LCP image on singular pages.
 * - Support content, blocks, widgets, excerpts, thumbnails, and embeds.
 *
 * @package RX_Theme
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'RX_Theme_Lazy_Loading' ) ) :

final class RX_Theme_Lazy_Loading {

	/**
	 * Internal image counter for the current page render.
	 *
	 * @var int
	 */
	private static int $image_index = 0;

	/**
	 * Internal iframe counter.
	 *
	 * @var int
	 */
	private static int $iframe_index = 0;

	/**
	 * Whether featured image preload was already printed.
	 *
	 * @var bool
	 */
	private static bool $printed_featured_preload = false;

	/**
	 * Initialize hooks.
	 *
	 * @return void
	 */
	public static function init(): void {

		if ( self::should_disable_all() ) {
			return;
		}

		add_action( 'after_setup_theme', array( __CLASS__, 'theme_support' ), 5 );

		/**
		 * Native WordPress media filters.
		 */
		add_filter( 'wp_lazy_loading_enabled', array( __CLASS__, 'native_lazy_loading_enabled' ), 20, 3 );
		add_filter( 'wp_get_attachment_image_attributes', array( __CLASS__, 'attachment_image_attributes' ), 20, 3 );
		add_filter( 'post_thumbnail_html', array( __CLASS__, 'post_thumbnail_html' ), 20, 5 );

		/**
		 * HTML/content processing.
		 */
		add_filter( 'the_content', array( __CLASS__, 'filter_html' ), 30 );
		add_filter( 'the_excerpt', array( __CLASS__, 'filter_html' ), 30 );
		add_filter( 'widget_text_content', array( __CLASS__, 'filter_html' ), 30 );
		add_filter( 'render_block', array( __CLASS__, 'filter_rendered_block' ), 20, 2 );
		add_filter( 'embed_oembed_html', array( __CLASS__, 'filter_embed_html' ), 20, 4 );

		/**
		 * Header optimization.
		 */
		add_action( 'wp_head', array( __CLASS__, 'preload_featured_image' ), 1 );
		add_action( 'wp_head', array( __CLASS__, 'print_lazy_loading_css' ), 30 );
		add_action( 'wp_footer', array( __CLASS__, 'print_lazy_loading_script' ), 30 );

		/**
		 * Optional body class.
		 */
		add_filter( 'body_class', array( __CLASS__, 'body_class' ) );
	}

	/**
	 * Add theme support.
	 *
	 * @return void
	 */
	public static function theme_support(): void {
		add_theme_support( 'responsive-embeds' );
		add_theme_support( 'post-thumbnails' );
	}

	/**
	 * Decide whether the whole module should be disabled.
	 *
	 * @return bool
	 */
	private static function should_disable_all(): bool {

		if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return true;
		}

		if ( is_feed() || is_preview() ) {
			return true;
		}

		if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) {
			return true;
		}

		/**
		 * Disable the whole RX lazy loading module.
		 *
		 * Example:
		 * add_filter( 'rx_lazy_loading_disable_all', '__return_true' );
		 */
		return (bool) apply_filters( 'rx_lazy_loading_disable_all', false );
	}

	/**
	 * Control WordPress native lazy loading.
	 *
	 * @param bool   $default  Default value.
	 * @param string $tag_name Tag name.
	 * @param string $context  Context.
	 *
	 * @return bool
	 */
	public static function native_lazy_loading_enabled( bool $default, string $tag_name, string $context ): bool {

		if ( self::should_disable_all() ) {
			return false;
		}

		if ( in_array( $tag_name, array( 'img', 'iframe' ), true ) ) {
			return true;
		}

		return $default;
	}

	/**
	 * Optimize images generated by wp_get_attachment_image().
	 *
	 * @param array        $attr       Image attributes.
	 * @param WP_Post     $attachment Attachment object.
	 * @param string|int[] $size       Requested size.
	 *
	 * @return array
	 */
	public static function attachment_image_attributes( array $attr, $attachment, $size ): array {

		if ( self::should_disable_all() ) {
			return $attr;
		}

		$attr['decoding'] = $attr['decoding'] ?? 'async';

		$classes = isset( $attr['class'] ) ? (string) $attr['class'] : '';

		if ( self::is_excluded_by_class( $classes ) ) {
			$attr['loading']       = 'eager';
			$attr['fetchpriority'] = $attr['fetchpriority'] ?? 'high';
			return $attr;
		}

		self::$image_index++;

		if ( self::$image_index <= self::get_eager_image_limit() ) {
			$attr['loading']       = 'eager';
			$attr['fetchpriority'] = $attr['fetchpriority'] ?? 'high';
		} else {
			$attr['loading']       = $attr['loading'] ?? 'lazy';
			$attr['fetchpriority'] = $attr['fetchpriority'] ?? 'low';
		}

		if ( empty( $attr['sizes'] ) ) {
			$attr['sizes'] = self::default_sizes_attr();
		}

		$attr['class'] = trim( $classes . ' rx-optimized-img' );

		return $attr;
	}

	/**
	 * Optimize post thumbnail HTML.
	 *
	 * @param string       $html              Thumbnail HTML.
	 * @param int          $post_id           Post ID.
	 * @param int          $post_thumbnail_id Thumbnail ID.
	 * @param string|int[] $size              Size.
	 * @param string|array $attr              Attr.
	 *
	 * @return string
	 */
	public static function post_thumbnail_html( string $html, int $post_id, int $post_thumbnail_id, $size, $attr ): string {

		if ( empty( $html ) || self::should_disable_all() ) {
			return $html;
		}

		/**
		 * Featured image is usually important for LCP.
		 */
		$html = self::modify_img_tags(
			$html,
			array(
				'force_eager_first'  => true,
				'force_high_first'   => true,
				'context'            => 'post_thumbnail',
				'add_rx_class'       => true,
				'count_global_index' => false,
			)
		);

		return $html;
	}

	/**
	 * Filter rendered Gutenberg blocks.
	 *
	 * @param string $block_content Block HTML.
	 * @param array  $block         Block data.
	 *
	 * @return string
	 */
	public static function filter_rendered_block( string $block_content, array $block ): string {

		if ( empty( $block_content ) || self::should_disable_all() ) {
			return $block_content;
		}

		$block_name = isset( $block['blockName'] ) ? (string) $block['blockName'] : '';

		/**
		 * Avoid over-processing navigation/search/login type blocks.
		 */
		$skip_blocks = apply_filters(
			'rx_lazy_loading_skip_blocks',
			array(
				'core/navigation',
				'core/search',
				'core/loginout',
			)
		);

		if ( in_array( $block_name, $skip_blocks, true ) ) {
			return $block_content;
		}

		return self::filter_html( $block_content );
	}

	/**
	 * Filter oEmbed HTML.
	 *
	 * @param string $html Embed HTML.
	 * @param string $url  URL.
	 * @param array  $attr Embed attributes.
	 * @param int    $post_id Post ID.
	 *
	 * @return string
	 */
	public static function filter_embed_html( string $html, string $url, array $attr, int $post_id ): string {

		if ( empty( $html ) || self::should_disable_all() ) {
			return $html;
		}

		return self::modify_iframe_tags(
			$html,
			array(
				'context' => 'embed_oembed_html',
			)
		);
	}

	/**
	 * Main HTML filter.
	 *
	 * @param string $html HTML.
	 *
	 * @return string
	 */
	public static function filter_html( string $html ): string {

		if ( empty( $html ) || self::should_disable_all() ) {
			return $html;
		}

		if ( false === stripos( $html, '<img' ) && false === stripos( $html, '<iframe' ) && false === stripos( $html, '<video' ) ) {
			return $html;
		}

		$html = self::modify_img_tags(
			$html,
			array(
				'context'            => current_filter(),
				'add_rx_class'       => true,
				'count_global_index' => true,
			)
		);

		$html = self::modify_iframe_tags(
			$html,
			array(
				'context' => current_filter(),
			)
		);

		$html = self::modify_video_tags(
			$html,
			array(
				'context' => current_filter(),
			)
		);

		return $html;
	}

	/**
	 * Modify img tags.
	 *
	 * @param string $html HTML.
	 * @param array  $args Args.
	 *
	 * @return string
	 */
	private static function modify_img_tags( string $html, array $args = array() ): string {

		$defaults = array(
			'context'            => 'rx_lazy_loading',
			'force_eager_first'  => false,
			'force_high_first'   => false,
			'add_rx_class'       => true,
			'count_global_index' => true,
		);

		$args = wp_parse_args( $args, $defaults );

		if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
			return self::modify_img_tags_with_processor( $html, $args );
		}

		return preg_replace_callback(
			'/<img\b[^>]*>/i',
			function ( array $matches ) use ( $args ): string {
				return self::modify_single_img_tag_fallback( $matches[0], $args );
			},
			$html
		);
	}

	/**
	 * Modify img tags with WP_HTML_Tag_Processor.
	 *
	 * @param string $html HTML.
	 * @param array  $args Args.
	 *
	 * @return string
	 */
	private static function modify_img_tags_with_processor( string $html, array $args ): string {

		$processor   = new WP_HTML_Tag_Processor( $html );
		$local_index = 0;

		while ( $processor->next_tag( 'img' ) ) {
			$local_index++;

			if ( ! empty( $args['count_global_index'] ) ) {
				self::$image_index++;
				$current_index = self::$image_index;
			} else {
				$current_index = $local_index;
			}

			$src   = (string) $processor->get_attribute( 'src' );
			$class = (string) $processor->get_attribute( 'class' );

			if ( self::is_bad_src( $src ) || self::is_excluded_by_class( $class ) ) {
				$processor->set_attribute( 'loading', 'eager' );
				$processor->set_attribute( 'decoding', 'async' );
				continue;
			}

			$is_first_or_lcp = (
				$current_index <= self::get_eager_image_limit()
				|| ( ! empty( $args['force_eager_first'] ) && 1 === $local_index )
			);

			$processor->set_attribute( 'decoding', 'async' );

			if ( $is_first_or_lcp ) {
				$processor->set_attribute( 'loading', 'eager' );

				if ( ! $processor->get_attribute( 'fetchpriority' ) || ! empty( $args['force_high_first'] ) ) {
					$processor->set_attribute( 'fetchpriority', 'high' );
				}
			} else {
				if ( ! $processor->get_attribute( 'loading' ) ) {
					$processor->set_attribute( 'loading', 'lazy' );
				}

				if ( ! $processor->get_attribute( 'fetchpriority' ) ) {
					$processor->set_attribute( 'fetchpriority', 'low' );
				}
			}

			if ( ! $processor->get_attribute( 'sizes' ) && $processor->get_attribute( 'srcset' ) ) {
				$processor->set_attribute( 'sizes', self::default_sizes_attr() );
			}

			if ( ! empty( $args['add_rx_class'] ) ) {
				$new_class = trim( $class . ' rx-optimized-img' );
				$processor->set_attribute( 'class', $new_class );
			}
		}

		return $processor->get_updated_html();
	}

	/**
	 * Fallback img modifier for older WordPress versions.
	 *
	 * @param string $tag  Img tag.
	 * @param array  $args Args.
	 *
	 * @return string
	 */
	private static function modify_single_img_tag_fallback( string $tag, array $args ): string {

		if ( self::tag_has_attribute( $tag, 'data-no-lazy' ) ) {
			return $tag;
		}

		$class = self::get_tag_attribute( $tag, 'class' );
		$src   = self::get_tag_attribute( $tag, 'src' );

		if ( self::is_bad_src( $src ) || self::is_excluded_by_class( $class ) ) {
			$tag = self::set_tag_attribute( $tag, 'loading', 'eager' );
			$tag = self::set_tag_attribute( $tag, 'decoding', 'async' );
			return $tag;
		}

		self::$image_index++;

		$is_first_or_lcp = self::$image_index <= self::get_eager_image_limit();

		if ( $is_first_or_lcp ) {
			$tag = self::set_tag_attribute( $tag, 'loading', 'eager' );
			$tag = self::set_tag_attribute( $tag, 'fetchpriority', 'high' );
		 } else {
			if ( ! self::tag_has_attribute( $tag, 'loading' ) ) {
				$tag = self::set_tag_attribute( $tag, 'loading', 'lazy' );
			}

			if ( ! self::tag_has_attribute( $tag, 'fetchpriority' ) ) {
				$tag = self::set_tag_attribute( $tag, 'fetchpriority', 'low' );
			}
		}

		$tag = self::set_tag_attribute( $tag, 'decoding', 'async' );

		if ( ! self::tag_has_attribute( $tag, 'sizes' ) && self::tag_has_attribute( $tag, 'srcset' ) ) {
			$tag = self::set_tag_attribute( $tag, 'sizes', self::default_sizes_attr() );
		}

		if ( ! empty( $args['add_rx_class'] ) ) {
			$tag = self::add_class_to_tag( $tag, 'rx-optimized-img' );
		}

		return $tag;
	}

	/**
	 * Modify iframe tags.
	 *
	 * @param string $html HTML.
	 * @param array  $args Args.
	 *
	 * @return string
	 */
	private static function modify_iframe_tags( string $html, array $args = array() ): string {

		if ( false === stripos( $html, '<iframe' ) ) {
			return $html;
		}

		if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
			$processor = new WP_HTML_Tag_Processor( $html );

			while ( $processor->next_tag( 'iframe' ) ) {
				self::$iframe_index++;

				$src   = (string) $processor->get_attribute( 'src' );
				$class = (string) $processor->get_attribute( 'class' );

				if ( self::is_bad_src( $src ) || self::is_excluded_by_class( $class ) ) {
					continue;
				}

				if ( ! $processor->get_attribute( 'loading' ) ) {
					$processor->set_attribute( 'loading', 'lazy' );
				}

				if ( ! $processor->get_attribute( 'title' ) ) {
					$processor->set_attribute( 'title', esc_attr__( 'Embedded content', 'rx-theme' ) );
				}

				$processor->set_attribute( 'referrerpolicy', 'strict-origin-when-cross-origin' );

				if ( ! $processor->get_attribute( 'allowfullscreen' ) ) {
					$processor->set_attribute( 'allowfullscreen', 'allowfullscreen' );
				}

				$new_class = trim( $class . ' rx-optimized-iframe' );
				$processor->set_attribute( 'class', $new_class );
			}

			return $processor->get_updated_html();
		}

		return preg_replace_callback(
			'/<iframe\b[^>]*>/i',
			function ( array $matches ): string {
				$tag = $matches[0];

				if ( self::tag_has_attribute( $tag, 'data-no-lazy' ) ) {
					return $tag;
				}

				if ( ! self::tag_has_attribute( $tag, 'loading' ) ) {
					$tag = self::set_tag_attribute( $tag, 'loading', 'lazy' );
				}

				if ( ! self::tag_has_attribute( $tag, 'title' ) ) {
					$tag = self::set_tag_attribute( $tag, 'title', esc_attr__( 'Embedded content', 'rx-theme' ) );
				}

				$tag = self::set_tag_attribute( $tag, 'referrerpolicy', 'strict-origin-when-cross-origin' );
				$tag = self::add_class_to_tag( $tag, 'rx-optimized-iframe' );

				return $tag;
			},
			$html
		);
	}

	/**
	 * Modify video tags.
	 *
	 * @param string $html HTML.
	 * @param array  $args Args.
	 *
	 * @return string
	 */
	private static function modify_video_tags( string $html, array $args = array() ): string {

		if ( false === stripos( $html, '<video' ) ) {
			return $html;
		}

		if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
			$processor = new WP_HTML_Tag_Processor( $html );

			while ( $processor->next_tag( 'video' ) ) {
				$class = (string) $processor->get_attribute( 'class' );

				if ( self::is_excluded_by_class( $class ) ) {
					continue;
				}

				if ( ! $processor->get_attribute( 'preload' ) ) {
					$processor->set_attribute( 'preload', 'metadata' );
				}

				$new_class = trim( $class . ' rx-optimized-video' );
				$processor->set_attribute( 'class', $new_class );
			}

			return $processor->get_updated_html();
		}

		return preg_replace_callback(
			'/<video\b[^>]*>/i',
			function ( array $matches ): string {
				$tag = $matches[0];

				if ( ! self::tag_has_attribute( $tag, 'preload' ) ) {
					$tag = self::set_tag_attribute( $tag, 'preload', 'metadata' );
				}

				return self::add_class_to_tag( $tag, 'rx-optimized-video' );
			},
			$html
		);
	}

	/**
	 * Preload featured image for singular pages.
	 *
	 * @return void
	 */
	public static function preload_featured_image(): void {

		if ( self::$printed_featured_preload || self::should_disable_all() ) {
			return;
		}

		if ( ! is_singular() || ! has_post_thumbnail() ) {
			return;
		}

		$post_id      = get_queried_object_id();
		$thumbnail_id = get_post_thumbnail_id( $post_id );

		if ( ! $thumbnail_id ) {
			return;
		}

		$src = wp_get_attachment_image_url( $thumbnail_id, 'full' );

		if ( ! $src ) {
			return;
		}

		$srcset = wp_get_attachment_image_srcset( $thumbnail_id, 'full' );
		$sizes  = wp_get_attachment_image_sizes( $thumbnail_id, 'full' );

		/**
		 * Disable featured image preload if needed.
		 */
		if ( ! apply_filters( 'rx_lazy_loading_preload_featured_image', true, $thumbnail_id, $post_id ) ) {
			return;
		}

		self::$printed_featured_preload = true;

		echo "\n" . '<link rel="preload" as="image" href="' . esc_url( $src ) . '"';

		if ( $srcset ) {
			echo ' imagesrcset="' . esc_attr( $srcset ) . '"';
		}

		if ( $sizes ) {
			echo ' imagesizes="' . esc_attr( $sizes ) . '"';
		}

		echo ' fetchpriority="high">' . "\n";
	}

	/**
	 * Print tiny CSS for lazy media.
	 *
	 * @return void
	 */
	public static function print_lazy_loading_css(): void {

		if ( self::should_disable_all() ) {
			return;
		}

		if ( ! apply_filters( 'rx_lazy_loading_print_css', true ) ) {
			return;
		}

		?>
<style id="rx-lazy-loading-css">
.rx-optimized-img {
	max-width: 100%;
	height: auto;
}

.rx-optimized-iframe,
.rx-optimized-video {
	max-width: 100%;
}

.rx-lazy-bg {
	background-size: cover;
	background-position: center;
	background-repeat: no-repeat;
}

.rx-lazy-bg[data-rx-bg] {
	background-image: none !important;
}
</style>
		<?php
	}

	/**
	 * Print tiny JS for optional lazy background images.
	 *
	 * Usage:
	 * <div class="rx-lazy-bg" data-rx-bg="https://example.com/image.jpg"></div>
	 *
	 * @return void
	 */
	public static function print_lazy_loading_script(): void {

		if ( self::should_disable_all() ) {
			return;
		}

		if ( ! apply_filters( 'rx_lazy_loading_print_script', true ) ) {
			return;
		}

		?>
<script id="rx-lazy-loading-js">
(function () {
	'use strict';

	var lazyBackgrounds = document.querySelectorAll('[data-rx-bg]');

	if (!lazyBackgrounds.length) {
		return;
	}

	function loadBackground(el) {
		var bg = el.getAttribute('data-rx-bg');

		if (!bg) {
			return;
		}

		el.style.backgroundImage = 'url("' + bg.replace(/"/g, '\\"') + '")';
		el.removeAttribute('data-rx-bg');
		el.classList.add('rx-lazy-bg-loaded');
	}

	if ('IntersectionObserver' in window) {
		var observer = new IntersectionObserver(function (entries, obs) {
			entries.forEach(function (entry) {
				if (entry.isIntersecting) {
					loadBackground(entry.target);
					obs.unobserve(entry.target);
				}
			});
		}, {
			rootMargin: '300px 0px'
		});

		lazyBackgrounds.forEach(function (el) {
			observer.observe(el);
		});
	} else {
		lazyBackgrounds.forEach(loadBackground);
	}
})();
</script>
		<?php
	}

	/**
	 * Add body class.
	 *
	 * @param array $classes Body classes.
	 *
	 * @return array
	 */
	public static function body_class( array $classes ): array {
		$classes[] = 'rx-lazy-loading-enabled';
		return $classes;
	}

	/**
	 * Get number of first images that should stay eager.
	 *
	 * @return int
	 */
	private static function get_eager_image_limit(): int {

		$limit = is_front_page() || is_home() ? 2 : 1;

		/**
		 * Control how many first images should not be lazy-loaded.
		 *
		 * Example:
		 * add_filter( 'rx_lazy_loading_eager_image_limit', fn() => 2 );
		 */
		return max( 0, (int) apply_filters( 'rx_lazy_loading_eager_image_limit', $limit ) );
	}

	/**
	 * Default sizes attribute.
	 *
	 * @return string
	 */
	private static function default_sizes_attr(): string {
		return (string) apply_filters( 'rx_lazy_loading_default_sizes', '(max-width: 768px) 100vw, 768px' );
	}

	/**
	 * Excluded classes.
	 *
	 * @return array
	 */
	private static function excluded_classes(): array {

		return (array) apply_filters(
			'rx_lazy_loading_excluded_classes',
			array(
				'no-lazy',
				'nolazy',
				'skip-lazy',
				'lazy-disable',
				'data-no-lazy',
				'custom-logo',
				'site-logo',
				'avatar',
				'emoji',
				'wp-smiley',
				'rx-no-lazy',
				'rx-lcp',
				'rx-hero',
				'hero-image',
				'elementor-motion-effects-element',
				'slick-slide',
				'swiper-slide',
			)
		);
	}

	/**
	 * Check excluded by class.
	 *
	 * @param string $class Class string.
	 *
	 * @return bool
	 */
	private static function is_excluded_by_class( string $class ): bool {

		if ( '' === $class ) {
			return false;
		}

		foreach ( self::excluded_classes() as $excluded ) {
			if ( false !== stripos( $class, (string) $excluded ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check bad src.
	 *
	 * @param string $src Src.
	 *
	 * @return bool
	 */
	private static function is_bad_src( string $src ): bool {

		$src = trim( $src );

		if ( '' === $src ) {
			return true;
		}

		if ( str_starts_with( $src, 'data:' ) || str_starts_with( $src, 'blob:' ) ) {
			return true;
		}

		if ( false !== stripos( $src, 'base64' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check tag attribute.
	 *
	 * @param string $tag  HTML tag.
	 * @param string $attr Attribute.
	 *
	 * @return bool
	 */
	private static function tag_has_attribute( string $tag, string $attr ): bool {
		return (bool) preg_match( '/\s' . preg_quote( $attr, '/' ) . '(\s*=\s*["\'][^"\']*["\']|\s|>)/i', $tag );
	}

	/**
	 * Get tag attribute.
	 *
	 * @param string $tag  HTML tag.
	 * @param string $attr Attribute.
	 *
	 * @return string
	 */
	private static function get_tag_attribute( string $tag, string $attr ): string {

		if ( preg_match( '/\s' . preg_quote( $attr, '/' ) . '\s*=\s*["\']([^"\']*)["\']/i', $tag, $matches ) ) {
			return (string) $matches[1];
		}

		return '';
	}

	/**
	 * Set or replace tag attribute.
	 *
	 * @param string $tag   HTML tag.
	 * @param string $attr  Attribute.
	 * @param string $value Value.
	 *
	 * @return string
	 */
	private static function set_tag_attribute( string $tag, string $attr, string $value ): string {

		$attr  = sanitize_key( $attr );
		$value = esc_attr( $value );

		if ( self::tag_has_attribute( $tag, $attr ) ) {
			return preg_replace(
				'/\s' . preg_quote( $attr, '/' ) . '\s*=\s*["\'][^"\']*["\']/i',
				' ' . $attr . '="' . $value . '"',
				$tag
			);
		}

		return preg_replace( '/<([a-z0-9:-]+)/i', '<$1 ' . $attr . '="' . $value . '"', $tag, 1 );
	}

	/**
	 * Add class to tag.
	 *
	 * @param string $tag       HTML tag.
	 * @param string $new_class New class.
	 *
	 * @return string
	 */
	private static function add_class_to_tag( string $tag, string $new_class ): string {

		$new_class = sanitize_html_class( $new_class );

		if ( self::tag_has_attribute( $tag, 'class' ) ) {
			return preg_replace_callback(
				'/\sclass\s*=\s*["\']([^"\']*)["\']/i',
				function ( array $matches ) use ( $new_class ): string {
					$classes = trim( $matches[1] . ' ' . $new_class );
					$classes = implode( ' ', array_unique( array_filter( explode( ' ', $classes ) ) ) );
					return ' class="' . esc_attr( $classes ) . '"';
				},
				$tag,
				1
			);
		}

		return preg_replace( '/<([a-z0-9:-]+)/i', '<$1 class="' . esc_attr( $new_class ) . '"', $tag, 1 );
	}
}

RX_Theme_Lazy_Loading::init();

endif;

Then include it in your main functions.php:

/**
 * RX Theme performance files.
 */
$rx_lazy_loading_file = get_template_directory() . '/performance/lazy-loading.php';

if ( file_exists( $rx_lazy_loading_file ) ) {
	require_once $rx_lazy_loading_file;
}

You can mark any image as never lazy-loaded like this:

<img src="hero.jpg" class="rx-hero rx-lcp rx-no-lazy" alt="Hero image">

For lazy background images, use:

<div class="rx-lazy-bg" data-rx-bg="https://example.com/background.jpg"></div>

This file is advanced but still safe because it does not remove WordPress core behavior; it improves and extends it using theme-level filters.

Leave a Reply

Your email address will not be published. Required fields are marked *