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

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

- Page: https://pluginprobe.com/plugins/xspeed/1.0.9/code/includes/class-lazy-loader.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.9/raw/includes/class-lazy-loader.php
- Modified: 2026-07-14T10:10:50+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.0.9/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;

	/**
	 * 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 = preg_replace_callback(
				$tag_re( 'img' ),
				array( __CLASS__, 'rewrite_img' ),
				$work
			);
		}
		if ( ! empty( $opts['lazy_iframes'] ) ) {
			$work = preg_replace_callback(
				$tag_re( 'iframe' ),
				array( __CLASS__, 'rewrite_iframe' ),
				$work
			);
		}
		if ( ! empty( $opts['lazy_videos'] ) ) {
			$work = preg_replace_callback(
				$tag_re( 'video' ),
				array( __CLASS__, 'rewrite_video' ),
				$work
			);
		}

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

	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' );
	}

	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 ) {
				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;
			}
		}

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

	/**
	 * @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;
	}
}

```
