# xspeed/1.0.9/includes/modules/Lazy/LazyModule.php

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

- Page: https://pluginprobe.com/plugins/xspeed/1.0.9/code/includes/modules/Lazy/LazyModule.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.9/raw/includes/modules/Lazy/LazyModule.php
- Modified: 2026-07-16T11:41:06+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/modules/Lazy/LazyModule.php#L10-L20`.

```php
<?php
/**
 * Lazy module — defers img / iframe / video loading via native browser
 * lazy-load attributes. Also auto-fills missing image dimensions to
 * prevent CLS.
 *
 * What WordPress core does already (since 5.5):
 *   - Adds loading="lazy" to the_content images.
 *
 * What this module adds:
 *   - First N images get loading="eager" so the LCP isn't deferred.
 *   - Adds decoding="async" (core doesn't).
 *   - Lazy-loads iframes (core's iframe lazy was reverted).
 *   - preload="none" on <video> (closest thing to native video lazy).
 *   - Auto-fills missing width / height attributes (best CLS win).
 *   - Excludes by substring patterns (src or class match) — useful for
 *     hero banner classes, logo files, etc.
 *
 * Tier: Free per FEATURES.md "Images" §1-6 (LiteSpeed parity — all
 * Free in LS Cache).
 *
 * @package XSpeed
 */

declare(strict_types=1);

namespace XSpeed\Modules\Lazy;

defined( 'ABSPATH' ) || exit;

use XSpeed\Lazy_Loader;
use XSpeed\Module;

final class LazyModule extends Module {

	public const SLUG    = 'lazy';
	public const TIER    = self::TIER_FREE;
	public const VERSION = '1.0.0';

	public function ui_metadata(): array {
		return array(
			'label'        => 'Media & Fonts',
			'tab_label'    => 'Lazy Loading', // its own tab on the Media & Fonts page
			'icon'         => 'Image',
			'description'  => 'Control how images, iframes, videos, and web fonts load — lazy-loading, format optimization, and font display.',
			// Host page: Lazy Loading (this module) + Image Optimization (Pro)
			// + AI Suggestions (Pro) + Fonts (Free) as tabs — everything a page
			// loads on one page instead of separate rows (FBS-83633).
			'custom_panel' => 'MediaPanel',
		);
	}

	public function settings_schema(): array {
		return array(
			'lazy_images'            => array(
				'type'        => 'bool',
				'default'     => true,
				'label'       => 'Lazy-load Images',
				'description' => 'Add loading="lazy" + decoding="async" to <img> tags in post content. The first images on the page get loading="eager" so the LCP image is not deferred.',
			),
			'lazy_iframes'           => array(
				'type'        => 'bool',
				'default'     => true,
				'label'       => 'Lazy-load Iframes',
				'description' => 'Add loading="lazy" to <iframe> tags. Useful for YouTube / Vimeo embeds + map widgets that pull a lot of bytes.',
			),
			'lazy_videos'            => array(
				'type'        => 'bool',
				'default'     => true,
				'label'       => 'Lazy-load HTML5 Videos',
				'description' => 'Set preload="none" on self-hosted <video> tags. Browsers do not yet support loading="lazy" on video; preload="none" is the closest equivalent.',
			),
			'eager_first_n'          => array(
				'type'        => 'int',
				'default'     => 1,
				'min'         => 0,
				'max'         => 10,
				'label'       => 'Eager-load First N Images',
				'description' => 'How many images at the top of the post get loading="eager". 1 is usually right (the LCP hero image). 0 to lazy-load everything.',
			),
			'add_missing_dimensions' => array(
				'type'        => 'bool',
				'default'     => true,
				'label'       => 'Add Missing Image Dimensions',
				'description' => 'When an <img class="wp-image-N"> has no width/height, look the values up from the media library and inject them. Prevents the page-layout shift that hurts CLS scores.',
			),
			'excluded_images'        => array(
				'type'        => 'list',
				'default'     => array(),
				'item_type'   => 'string',
				'label'       => 'Excluded Images',
				'description' => 'Substring patterns that, if found anywhere in the <img> / <iframe> tag (typically a class or filename), exempt that element from lazy-loading. Useful for hero / logo / sprite images. Or add data-skip-lazy to the tag directly.',
			),
		);
	}

	public function conflicts(): array {
		return array(
			array(
				'plugin'   => 'wp-smushit/wp-smush.php',
				'feature'  => 'images.lazyload',
				'strategy' => \XSpeed\Conflict_Registry::STRATEGY_WARN,
				'reason'   => 'Smush also offers lazy-loading; running both can cause double-rewriting.',
			),
			array(
				'plugin'   => 'a3-lazy-load/a3-lazy-load.php',
				'feature'  => 'images.lazyload',
				'strategy' => \XSpeed\Conflict_Registry::STRATEGY_REFUSE,
				'reason'   => 'a3 Lazy Load is a dedicated lazy-load plugin; disable it before enabling xSpeed lazy-load.',
			),
		);
	}

	public function boot(): void {
		// Bail entirely on admin / feed / cron / REST — same scope as
		// Minifier. Lazy-loading rendered HTML only matters on real
		// frontend page renders.
		if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
			return;
		}

		$opts = $this->get_settings();
		$any_enabled = ! empty( $opts['lazy_images'] )
			|| ! empty( $opts['lazy_iframes'] )
			|| ! empty( $opts['lazy_videos'] )
			|| ! empty( $opts['add_missing_dimensions'] );
		if ( ! $any_enabled ) {
			return;
		}

		// Reset the eager-load budget once per page render, before any
		// content filter runs, so the "first N images eager" budget is
		// shared across the featured image + content + avatars rather than
		// restarting on every filter pass. (FBS-82172 Bug 1)
		add_action( 'template_redirect', array( Lazy_Loader::class, 'reset_state' ) );

		// Late priority so the_content runs after every other filter
		// (shortcodes, do_blocks, embeds). Avoids rewriting tags that
		// haven't been generated yet.
		add_filter( 'the_content',          array( Lazy_Loader::class, 'process_html' ), 999 );
		add_filter( 'post_thumbnail_html',  array( Lazy_Loader::class, 'process_html' ), 999 );
		add_filter( 'get_avatar',           array( Lazy_Loader::class, 'process_html' ), 999 );
		add_filter( 'widget_text_content',  array( Lazy_Loader::class, 'process_html' ), 999 );
	}

	public function cli_commands(): array {
		return array(
			array(
				'name'      => 'xspeed lazy',
				'callback'  => array( $this, 'cli_handler' ),
				'shortdesc' => 'Show which lazy-load toggles are active.',
				'synopsis'  => array(),
			),
		);
	}

	public function cli_handler( array $args, array $assoc ): void {
		$opts = $this->get_settings();
		foreach ( $opts as $key => $value ) {
			$display = is_array( $value ) ? implode( ',', $value ) : ( $value ? 'on' : ( is_numeric( $value ) ? (string) $value : 'off' ) );
			if ( is_int( $value ) ) {
				$display = (string) $value;
			}
			\WP_CLI::log( sprintf( '%-30s %s', $key, $display ) );
		}
	}
}

```
