PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.2
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / modules / Lazy / LazyModule.php

LazyModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.2, at includes/modules/Lazy/LazyModule.php

155 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Lazy module — defers img / iframe / video loading via native browser
4 * lazy-load attributes. Also auto-fills missing image dimensions to
5 * prevent CLS.
6 *
7 * What WordPress core does already (since 5.5):
8 * - Adds loading="lazy" to the_content images.
9 *
10 * What this module adds:
11 * - First N images get loading="eager" so the LCP isn't deferred.
12 * - Adds decoding="async" (core doesn't).
13 * - Lazy-loads iframes (core's iframe lazy was reverted).
14 * - preload="none" on <video> (closest thing to native video lazy).
15 * - Auto-fills missing width / height attributes (best CLS win).
16 * - Excludes by substring patterns (src or class match) — useful for
17 * hero banner classes, logo files, etc.
18 *
19 * Tier: Free per FEATURES.md "Images" §1-6 (LiteSpeed parity — all
20 * Free in LS Cache).
21 *
22 * @package XSpeed
23 */
24
25 declare(strict_types=1);
26
27 namespace XSpeed\Modules\Lazy;
28
29 use XSpeed\Lazy_Loader;
30 use XSpeed\Module;
31
32 final class LazyModule extends Module {
33
34 public const SLUG = 'lazy';
35 public const TIER = self::TIER_FREE;
36 public const VERSION = '1.0.0';
37
38 public function ui_metadata(): array {
39 return array(
40 'label' => 'Lazy Load',
41 'icon' => 'Image',
42 'description' => 'Defer images / iframes / videos until they scroll into view, and auto-add missing dimensions to prevent CLS.',
43 );
44 }
45
46 public function settings_schema(): array {
47 return array(
48 'lazy_images' => array(
49 'type' => 'bool',
50 'default' => true,
51 'label' => 'Lazy-load Images',
52 '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.',
53 ),
54 'lazy_iframes' => array(
55 'type' => 'bool',
56 'default' => true,
57 'label' => 'Lazy-load Iframes',
58 'description' => 'Add loading="lazy" to <iframe> tags. Useful for YouTube / Vimeo embeds + map widgets that pull a lot of bytes.',
59 ),
60 'lazy_videos' => array(
61 'type' => 'bool',
62 'default' => true,
63 'label' => 'Lazy-load HTML5 Videos',
64 'description' => 'Set preload="none" on self-hosted <video> tags. Browsers do not yet support loading="lazy" on video; preload="none" is the closest equivalent.',
65 ),
66 'eager_first_n' => array(
67 'type' => 'int',
68 'default' => 1,
69 'min' => 0,
70 'max' => 10,
71 'label' => 'Eager-load First N Images',
72 '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.',
73 ),
74 'add_missing_dimensions' => array(
75 'type' => 'bool',
76 'default' => true,
77 'label' => 'Add Missing Image Dimensions',
78 '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.',
79 ),
80 'excluded_images' => array(
81 'type' => 'list',
82 'default' => array(),
83 'item_type' => 'string',
84 'label' => 'Excluded Images',
85 '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.',
86 ),
87 );
88 }
89
90 public function conflicts(): array {
91 return array(
92 array(
93 'plugin' => 'wp-smushit/wp-smush.php',
94 'feature' => 'images.lazyload',
95 'strategy' => \XSpeed\Conflict_Registry::STRATEGY_WARN,
96 'reason' => 'Smush also offers lazy-loading; running both can cause double-rewriting.',
97 ),
98 array(
99 'plugin' => 'a3-lazy-load/a3-lazy-load.php',
100 'feature' => 'images.lazyload',
101 'strategy' => \XSpeed\Conflict_Registry::STRATEGY_REFUSE,
102 'reason' => 'a3 Lazy Load is a dedicated lazy-load plugin; disable it before enabling xSpeed lazy-load.',
103 ),
104 );
105 }
106
107 public function boot(): void {
108 // Bail entirely on admin / feed / cron / REST — same scope as
109 // Minifier. Lazy-loading rendered HTML only matters on real
110 // frontend page renders.
111 if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
112 return;
113 }
114
115 $opts = $this->get_settings();
116 $any_enabled = ! empty( $opts['lazy_images'] )
117 || ! empty( $opts['lazy_iframes'] )
118 || ! empty( $opts['lazy_videos'] )
119 || ! empty( $opts['add_missing_dimensions'] );
120 if ( ! $any_enabled ) {
121 return;
122 }
123
124 // Late priority so the_content runs after every other filter
125 // (shortcodes, do_blocks, embeds). Avoids rewriting tags that
126 // haven't been generated yet.
127 add_filter( 'the_content', array( Lazy_Loader::class, 'process_html' ), 999 );
128 add_filter( 'post_thumbnail_html', array( Lazy_Loader::class, 'process_html' ), 999 );
129 add_filter( 'get_avatar', array( Lazy_Loader::class, 'process_html' ), 999 );
130 add_filter( 'widget_text_content', array( Lazy_Loader::class, 'process_html' ), 999 );
131 }
132
133 public function cli_commands(): array {
134 return array(
135 array(
136 'name' => 'xspeed lazy',
137 'callback' => array( $this, 'cli_handler' ),
138 'shortdesc' => 'Show which lazy-load toggles are active.',
139 'synopsis' => array(),
140 ),
141 );
142 }
143
144 public function cli_handler( array $args, array $assoc ): void {
145 $opts = $this->get_settings();
146 foreach ( $opts as $key => $value ) {
147 $display = is_array( $value ) ? implode( ',', $value ) : ( $value ? 'on' : ( is_numeric( $value ) ? (string) $value : 'off' ) );
148 if ( is_int( $value ) ) {
149 $display = (string) $value;
150 }
151 \WP_CLI::log( sprintf( '%-30s %s', $key, $display ) );
152 }
153 }
154 }
155