# xspeed/1.1.2/includes/class-lazy-loader.php

xSpeed Cache: AI-Powered Performance Hub with MCP, Caching &amp; CDN, version 1.1.2. 523 lines.

- Page: https://pluginprobe.com/plugins/xspeed/1.1.2/code/includes/class-lazy-loader.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.1.2/raw/includes/class-lazy-loader.php
- Modified: 2026-07-30T20:04:12+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/xspeed/1.1.2/code/includes/class-lazy-loader.php#L10-L20`.

```php
<?php
/**
 * Lazy_Loader — rewrites img / iframe / video tags in rendered HTML to
 * add native `loading="lazy"` (or "eager" for above-the-fold) plus
 * `decoding="async"` on images. Also auto-adds missing width/height
 * attributes to prevent CLS.
 *
 * Why regex instead of DOMDocument:
 *   - DOMDocument forces a full HTML5 parse round trip per filter call;
 *     on a content-heavy post that's measurably slow. Regex over the
 *     specific tags is ~10× faster.
 *   - We don't need full DOM understanding — every rewrite is a tag-
 *     local attribute injection. Regex is sufficient + predictable.
 *   - Edge cases (img inside HTML comments, img in <script>) are rare
 *     in real post content; we leave those alone with a pre-pass that
 *     stubs out script / style / pre blocks before rewriting.
 *
 * @package XSpeed
 */

declare(strict_types=1);

namespace XSpeed;

defined( 'ABSPATH' ) || exit;

final class Lazy_Loader {

	/**
	 * In-process counter for above-the-fold skipping. Reset by
	 * process_html on every call so a fresh post starts at 0.
	 *
	 * @var int
	 */
	private static $image_counter = 0;

	/**
	 * Settings cache (one read per request).
	 *
	 * @var array|null
	 */
	private static $opts = null;

	/**
	 * Per-URL dimension cache (md5(src) => [w,h] | 0 for known-failure),
	 * hydrated from the `xspeed_img_dims` transient once per request.
	 *
	 * @var array<string,mixed>|null
	 */
	private static $src_dims_cache = null;

	/**
	 * Main entry point: take rendered HTML, return rewritten HTML.
	 * Pure function aside from the static counters.
	 */
	public static function process_html( string $html ): string {
		if ( '' === $html ) {
			return $html;
		}
		$opts = self::opts();

		// NOTE: the eager-load budget counter is NOT reset here. process_html
		// runs once per filter pass — the_content, post_thumbnail_html, and
		// once per get_avatar — so resetting per call let the featured image,
		// the first content image, AND every comment avatar each claim an
		// "eager" slot, defeating the budget. The counter is reset once per
		// page render via reset_state() on template_redirect, so it now
		// accumulates across all passes as intended. (FBS-82172 Bug 1)

		// Stub out <script>, <style>, <noscript>, <pre>, <code> blocks
		// so img tags embedded in them as text examples aren't
		// rewritten. Restore after pass.
		[ $work, $stubs ] = self::stub_safe_blocks( $html );

		// Tag matcher that respects quoted attribute values, so a ">" inside
		// an attribute (e.g. alt="a > b") doesn't end the match early and
		// corrupt the tag. Matches: double-quoted runs, single-quoted runs,
		// or any non-> char — repeated up to the real closing >.
		// (FBS-82172 Bug 3)
		$tag_re = static function ( string $name ): string {
			return '#<' . $name . '\b(?:"[^"]*"|\'[^\']*\'|[^>"\'])*>#i';
		};

		if ( ! empty( $opts['lazy_images'] ) || ! empty( $opts['add_missing_dimensions'] ) ) {
			$work = self::apply_pass( $work, $tag_re( 'img' ), array( __CLASS__, 'rewrite_img' ) );
		}
		if ( ! empty( $opts['lazy_iframes'] ) ) {
			$work = self::apply_pass( $work, $tag_re( 'iframe' ), array( __CLASS__, 'rewrite_iframe' ) );
		}
		// Facade runs AFTER the lazy pass, deliberately. The facade keeps the
		// original tag inside <noscript> as the JS-less fallback, and that
		// fallback should carry loading="lazy" too — running this first would
		// produce an eager iframe for exactly the visitors least able to
		// afford one.
		//
		// Unlike every other pass here, the facade REPLACES the element
		// rather than injecting attributes into its opening tag — so it has
		// to consume the whole element, `</iframe>` included. Matching the
		// opening tag alone orphaned the closing tag outside the injected
		// <noscript>, which broke nesting and swallowed sibling content in
		// real browsers. The body is tempered (`(?!</?iframe\b)`) so an
		// unclosed iframe can't make the match run on to a LATER embed's
		// closing tag and eat everything in between; an iframe with no
		// closing tag simply doesn't match and passes through untouched.
		if ( ! empty( $opts['video_facade'] ) ) {
			$work = self::apply_pass(
				$work,
				'#(<iframe\b(?:"[^"]*"|\'[^\']*\'|[^>"\'])*>)((?:(?!</?iframe\b).)*)</iframe\s*>#is',
				array( __CLASS__, 'rewrite_iframe_facade' )
			);
		}
		if ( ! empty( $opts['lazy_videos'] ) ) {
			$work = self::apply_pass( $work, $tag_re( 'video' ), array( __CLASS__, 'rewrite_video' ) );
		}

		return self::restore_safe_blocks( $work, $stubs );
	}

	/**
	 * Run one rewrite pass, keeping the input if PCRE bails.
	 *
	 * preg_replace_callback() returns null when it hits the backtrack or
	 * recursion limit — on a large page that would otherwise blank the
	 * whole document. Returning the untouched HTML costs the optimization
	 * for that request and nothing else.
	 *
	 * @param callable $callback Rewrite callback for one match.
	 */
	private static function apply_pass( string $html, string $pattern, callable $callback ): string {
		$result = preg_replace_callback( $pattern, $callback, $html );

		return is_string( $result ) ? $result : $html;
	}

	private static function rewrite_img( array $m ): string {
		$tag  = $m[0];
		$opts = self::opts();

		// Explicit skip flag, or matches an exclusion pattern: opt OUT of
		// LAZY-LOADING only. Dimension injection (CLS protection) still
		// applies — excluding an above-the-fold hero/logo from lazy-load is
		// exactly when you most want its width/height kept. Previously both
		// of these returned early, silently stripping dimensions too.
		// (FBS-82172 Bug 2)
		$skip_lazy = false !== stripos( $tag, 'data-skip-lazy' )
			|| false !== stripos( $tag, 'data-no-lazy' )
			|| self::is_excluded( $tag, $opts );

		if ( $skip_lazy && ! empty( $opts['lazy_images'] ) ) {
			// An EXCLUDED image is one the user marked as above-the-fold (a
			// hero/logo) — the opposite of lazy. WordPress core adds
			// `loading="lazy"` to images by default (since 5.5), so merely
			// *skipping* our lazy pass would leave core's lazy attribute on
			// the LCP hero and tank LCP. Actively make it eager +
			// high-priority so an excluded hero loads immediately.
			$tag = self::set_attr( $tag, 'loading', 'eager' );
			$tag = self::set_attr( $tag, 'fetchpriority', 'high', true );
			$tag = self::set_attr( $tag, 'decoding', 'async', true );
		} elseif ( ! empty( $opts['lazy_images'] ) ) {
			// Above-the-fold skip: first N images get loading="eager"
			// instead of "lazy" so the LCP image isn't deferred. Only
			// non-excluded images consume the budget.
			self::$image_counter++;
			$is_above_fold = self::$image_counter <= max( 0, (int) ( $opts['eager_first_n'] ?? 1 ) );
			$tag           = self::set_attr( $tag, 'loading', $is_above_fold ? 'eager' : 'lazy' );
			$tag           = self::set_attr( $tag, 'decoding', 'async', true );
			// The eager hero should also drop any core `loading="lazy"`; the
			// set_attr above already overrode it. Give the first eager image
			// high fetch priority so it wins the LCP race.
			if ( $is_above_fold ) {
				$tag = self::set_attr( $tag, 'fetchpriority', 'high', true );
			}
		}

		if ( ! empty( $opts['add_missing_dimensions'] ) ) {
			$tag = self::ensure_dimensions( $tag );
		}

		return $tag;
	}

	private static function rewrite_iframe( array $m ): string {
		$tag = $m[0];
		if ( false !== stripos( $tag, 'data-skip-lazy' ) ) {
			return $tag;
		}
		if ( self::is_excluded( $tag, self::opts() ) ) {
			return $tag;
		}
		return self::set_attr( $tag, 'loading', 'lazy' );
	}

	/**
	 * Swap a recognised video embed for a click-to-play facade.
	 *
	 * Passes the element through untouched unless it is a provider we can
	 * build a facade for — an unknown iframe (a map, a form, a dashboard)
	 * must never be replaced by a play button.
	 *
	 * $m[0] is the WHOLE element (`<iframe …>…</iframe>`); $m[1] is just
	 * the opening tag. Attributes are read from the opening tag, but what
	 * goes into the <noscript> fallback — and what is returned on every
	 * bail-out path — is the whole element, so the closing tag is never
	 * left stranded outside it.
	 */
	private static function rewrite_iframe_facade( array $m ): string {
		$element = $m[0];
		$tag     = $m[1];

		if ( false !== stripos( $tag, 'data-skip-lazy' ) ) {
			return $element;
		}
		if ( self::is_excluded( $tag, self::opts() ) ) {
			return $element;
		}

		if ( ! preg_match( '#\bsrc\s*=\s*(["\'])(.*?)\1#i', $tag, $src_m ) ) {
			return $element;
		}
		$src = $src_m[2];

		$embed = Video_Facade::parse_embed( $src );
		if ( null === $embed ) {
			return $element;
		}

		$title = '';
		if ( preg_match( '#\btitle\s*=\s*(["\'])(.*?)\1#i', $tag, $title_m ) ) {
			$title = $title_m[2];
		}

		self::$facade_used = true;

		return Video_Facade::render( $element, $embed, $src, $title );
	}

	/** @var bool True once a facade has been rendered on this page. */
	private static $facade_used = false;

	/**
	 * True when this render produced at least one facade — the module uses
	 * it to decide whether the click handler is worth printing at all.
	 */
	public static function facade_used(): bool {
		return self::$facade_used;
	}

	private static function rewrite_video( array $m ): string {
		$tag = $m[0];
		if ( false !== stripos( $tag, 'data-skip-lazy' ) ) {
			return $tag;
		}
		// HTML5 `<video>` doesn't support loading=lazy yet (Chromium
		// won't add it before there's broad support). What we CAN do
		// is set preload="none" so the browser doesn't pre-fetch the
		// video bytes until play is requested — that's the actual win
		// users want from "lazy-load videos".
		if ( false === stripos( $tag, 'preload=' ) ) {
			$tag = self::set_attr( $tag, 'preload', 'none' );
		}
		return $tag;
	}

	/**
	 * Add an attribute to an opening tag if it isn't already present.
	 * Pass $only_if_missing=false to override an existing value (e.g.
	 * flipping loading="lazy" → "eager" on the first image).
	 */
	private static function set_attr( string $tag, string $name, string $value, bool $only_if_missing = false ): string {
		$pattern = '#\b' . preg_quote( $name, '#' ) . '\s*=\s*(["\'][^"\']*["\']|\S+)#i';
		if ( preg_match( $pattern, $tag ) ) {
			if ( $only_if_missing ) {
				return $tag;
			}
			return (string) preg_replace( $pattern, $name . '="' . $value . '"', $tag, 1 );
		}
		// Inject before the closing > (preserving self-closing `/>` if present).
		if ( preg_match( '#(/?>)$#', $tag, $m ) ) {
			$close = $m[1];
			return substr( $tag, 0, -strlen( $close ) ) . ' ' . $name . '="' . $value . '"' . $close;
		}
		return $tag;
	}

	/**
	 * Attempt to fill in missing width / height from either an attached
	 * media library record (when class="wp-image-N") or from the local
	 * filesystem when src points at the uploads dir. Skip when we can't
	 * resolve cheaply — never block the request on a remote getimagesize.
	 */
	private static function ensure_dimensions( string $tag ): string {
		$has_w = (bool) preg_match( '#\bwidth\s*=#i', $tag );
		$has_h = (bool) preg_match( '#\bheight\s*=#i', $tag );
		if ( $has_w && $has_h ) {
			return $tag;
		}

		// Try wp-image-<id> class first (cheapest path; one DB-cached
		// get_post_meta call).
		if ( preg_match( '#\bclass\s*=\s*["\']([^"\']*)["\']#i', $tag, $cm ) && preg_match( '#wp-image-(\d+)#i', $cm[1], $idm ) ) {
			$dims = self::dimensions_for_attachment( (int) $idm[1] );
			if ( $dims ) {
				return self::apply_dimensions( $tag, $dims, $has_w, $has_h );
			}
		}

		// No wp-image-N class — page-builder markup (Essential Blocks and
		// friends) never emits it, which is why the setting silently failed
		// on those images (issue #37). Resolve from the src instead, but only
		// when the tag doesn't already tell us it renders at some other size:
		// stamping the intrinsic file size onto a responsive or CSS-sized
		// image would CREATE the layout shift this feature exists to remove.
		if ( ! self::has_constrained_render( $tag ) && preg_match( '#\bsrc\s*=\s*["\']([^"\']+)["\']#i', $tag, $sm ) ) {
			$dims = self::dimensions_for_src( $sm[1] );
			if ( $dims ) {
				return self::apply_dimensions( $tag, $dims, $has_w, $has_h );
			}
		}

		// Couldn't resolve. Leave the tag alone — better no dimensions
		// than wrong ones.
		return $tag;
	}

	/**
	 * True when the tag says it renders at a size other than the file's
	 * intrinsic one — a `srcset`/`sizes` pair (the browser picks a
	 * candidate) or an inline width/height style.
	 *
	 * Only guards the src-suffix fallback. The `wp-image-N` path stays
	 * unguarded: attachment metadata is authoritative, and WordPress'
	 * own `wp_filter_content_tags()` adds dimensions to responsive
	 * images the same way. Pure — unit-tested.
	 */
	public static function has_constrained_render( string $tag ): bool {
		if ( preg_match( '#\bsrcset\s*=#i', $tag ) || preg_match( '#\bsizes\s*=#i', $tag ) ) {
			return true;
		}
		if ( preg_match( '#\bstyle\s*=\s*["\']([^"\']*)["\']#i', $tag, $m ) ) {
			// width/height in the inline style wins over the attribute, so
			// the file's intrinsic size would disagree with the layout.
			return 1 === preg_match( '#(?:^|;)\s*(?:max-)?(?:width|height)\s*:#i', $m[1] );
		}
		return false;
	}

	/** @param int[] $dims [width, height]. */
	private static function apply_dimensions( string $tag, array $dims, bool $has_w, bool $has_h ): string {
		if ( ! $has_w ) {
			$tag = self::set_attr( $tag, 'width', (string) $dims[0] );
		}
		if ( ! $has_h ) {
			$tag = self::set_attr( $tag, 'height', (string) $dims[1] );
		}
		return $tag;
	}

	/**
	 * WordPress names resized files `<name>-WxH.<ext>` — when the suffix is
	 * present it IS the rendered size, resolvable with zero I/O (works for
	 * CDN-hosted copies too). Pure — unit-tested.
	 *
	 * @return int[]|null [width, height] or null.
	 */
	public static function parse_size_suffix( string $src ): ?array {
		$path = (string) preg_replace( '/[?#].*$/', '', $src );
		if ( preg_match( '#-(\d{1,4})x(\d{1,4})\.(?:jpe?g|png|gif|webp|avif)$#i', $path, $m ) ) {
			$w = (int) $m[1];
			$h = (int) $m[2];
			if ( $w > 0 && $h > 0 ) {
				return array( $w, $h );
			}
		}
		return null;
	}

	/**
	 * Resolve dimensions from an image URL, cheapest first:
	 *   1. `-WxH` filename suffix (no I/O).
	 *   2. Intrinsic size of the local file when src is under uploads
	 *      (getimagesize on the header — no remote fetches, ever).
	 *   3. Attachment lookup by URL (uploads-hosted src only).
	 * Results — including failures — are cached per URL in a bounded
	 * transient so each image pays the lookup once, not per pageview.
	 *
	 * @return int[]|null [width, height] or null.
	 */
	private static function dimensions_for_src( string $src ): ?array {
		$suffix = self::parse_size_suffix( $src );
		if ( $suffix ) {
			return $suffix;
		}

		if ( ! function_exists( 'wp_get_upload_dir' ) || ! function_exists( 'get_transient' ) ) {
			return null;
		}
		$uploads = wp_get_upload_dir();
		$baseurl = isset( $uploads['baseurl'] ) ? (string) $uploads['baseurl'] : '';
		$basedir = isset( $uploads['basedir'] ) ? (string) $uploads['basedir'] : '';
		if ( '' === $baseurl || '' === $basedir || 0 !== strpos( $src, $baseurl ) ) {
			return null; // External image — never fetch remotely for a size.
		}

		if ( null === self::$src_dims_cache ) {
			$stored               = get_transient( 'xspeed_img_dims' );
			self::$src_dims_cache = is_array( $stored ) ? $stored : array();
		}
		$key = md5( $src );
		if ( array_key_exists( $key, self::$src_dims_cache ) ) {
			$hit = self::$src_dims_cache[ $key ];
			return is_array( $hit ) ? $hit : null; // 0 = cached failure.
		}

		$dims     = null;
		$relative = (string) preg_replace( '/[?#].*$/', '', substr( $src, strlen( $baseurl ) ) );
		if ( false === strpos( $relative, '..' ) ) {
			$file = $basedir . $relative;
			if ( is_file( $file ) ) {
				$size = @getimagesize( $file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- non-image/corrupt file must degrade to null, not warn.
				if ( is_array( $size ) && ! empty( $size[0] ) && ! empty( $size[1] ) ) {
					$dims = array( (int) $size[0], (int) $size[1] );
				}
			}
		}

		// File not on disk (offloaded originals) — one DB lookup by URL.
		if ( null === $dims && function_exists( 'attachment_url_to_postid' ) ) {
			$id = (int) attachment_url_to_postid( $src );
			if ( $id > 0 ) {
				$dims = self::dimensions_for_attachment( $id );
			}
		}

		// Cache success AND failure (0), bounded so the blob can't grow
		// unbounded on media-heavy sites.
		if ( count( self::$src_dims_cache ) >= 500 ) {
			self::$src_dims_cache = array_slice( self::$src_dims_cache, 250, null, true );
		}
		self::$src_dims_cache[ $key ] = null === $dims ? 0 : $dims;
		if ( function_exists( 'set_transient' ) ) {
			set_transient( 'xspeed_img_dims', self::$src_dims_cache, DAY_IN_SECONDS );
		}
		return $dims;
	}

	/**
	 * @return int[]|null [width, height] or null
	 */
	private static function dimensions_for_attachment( int $attachment_id ): ?array {
		if ( ! function_exists( 'wp_get_attachment_metadata' ) ) {
			return null;
		}
		$meta = wp_get_attachment_metadata( $attachment_id );
		if ( ! is_array( $meta ) || empty( $meta['width'] ) || empty( $meta['height'] ) ) {
			return null;
		}
		return array( (int) $meta['width'], (int) $meta['height'] );
	}

	private static function is_excluded( string $tag, array $opts ): bool {
		$excluded = $opts['excluded_images'] ?? array();
		if ( ! is_array( $excluded ) || empty( $excluded ) ) {
			return false;
		}
		foreach ( $excluded as $pattern ) {
			$pattern = (string) $pattern;
			if ( '' === $pattern ) {
				continue;
			}
			if ( false !== stripos( $tag, $pattern ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Replace <script>, <style>, <noscript>, <pre>, <code> blocks with
	 * placeholder tokens before tag rewriting. Returns [stubbed_html,
	 * stubs_map]. Restore via restore_safe_blocks().
	 *
	 * @return array{0: string, 1: array<string,string>}
	 */
	private static function stub_safe_blocks( string $html ): array {
		$stubs = array();
		$re    = '#<(script|style|noscript|pre|code)\b[^>]*>.*?</\1>#is';
		$out   = preg_replace_callback(
			$re,
			static function ( $m ) use ( &$stubs ) {
				$key            = '<!--XSPEED_LAZY_STUB_' . count( $stubs ) . '-->';
				$stubs[ $key ] = $m[0];
				return $key;
			},
			$html
		);
		return array( (string) $out, $stubs );
	}

	private static function restore_safe_blocks( string $html, array $stubs ): string {
		if ( empty( $stubs ) ) {
			return $html;
		}
		return strtr( $html, $stubs );
	}

	private static function opts(): array {
		if ( null === self::$opts ) {
			self::$opts = Settings_Manager::get( 'lazy' );
		}
		return self::$opts;
	}

	/**
	 * Test-only: clear cached opts + counter between assertions.
	 */
	public static function reset_state(): void {
		self::$opts           = null;
		self::$image_counter  = 0;
		self::$src_dims_cache = null;
		self::$facade_used    = false;
	}
}

```
