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.