PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.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
← All changes | includes/modules/Lazy/LazyModule.php +103 -19 1.1.21.3.2 View file →
@@ -38,15 +38,16 @@
38 38 public const VERSION = '1.0.0';
39 39
40 40 public function ui_metadata(): array {
41 41 return array(
42 - 'label' => 'Media & Fonts',
43 - 'tab_label' => 'Lazy Loading', // its own tab on the Media & Fonts page
42 + 'label' => __( 'Media Optimization', 'xspeed' ),
43 + 'tab_label' => __( 'Lazy Loading', 'xspeed' ), // its own tab on the Media Optimization page
44 44 'icon' => 'Image',
45 - 'description' => 'Control how images, iframes, videos, and web fonts load — lazy-loading, format optimization, and font display.',
45 + 'description' => __( 'Control how images, iframes, and videos load — lazy-loading, missing dimensions, and format optimization.', 'xspeed' ),
46 46 // Host page: Lazy Loading (this module) + Image Optimization (Pro)
47 - // + AI Suggestions (Pro) + Fonts (Free) as tabs — everything a page
48 - // loads on one page instead of separate rows (FBS-83633).
47 + // + AI Suggestions (Pro) as tabs — everything a page loads on one
48 + // page instead of separate rows (FBS-83633). Fonts is NOT here: it
49 + // has its own Optimization card (#86).
49 50 'custom_panel' => 'MediaPanel',
50 51 );
51 52 }
52 53
@@ -54,28 +55,28 @@
54 55 return array(
55 56 'lazy_images' => array(
56 57 'type' => 'bool',
57 58 'default' => true,
58 - 'label' => 'Lazy-load Images',
59 - '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.',
59 + 'label' => __( 'Lazy-load Images', 'xspeed' ),
60 + '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.', 'xspeed' ),
60 61 ),
61 62 'lazy_iframes' => array(
62 63 'type' => 'bool',
63 64 'default' => true,
64 - 'label' => 'Lazy-load Iframes',
65 - 'description' => 'Add loading="lazy" to <iframe> tags. Useful for YouTube / Vimeo embeds + map widgets that pull a lot of bytes.',
65 + 'label' => __( 'Lazy-load Iframes', 'xspeed' ),
66 + 'description' => __( 'Add loading="lazy" to <iframe> tags. Useful for YouTube / Vimeo embeds + map widgets that pull a lot of bytes.', 'xspeed' ),
66 67 ),
67 68 'video_facade' => array(
68 69 'type' => 'bool',
69 70 'default' => false,
70 - 'label' => 'Click-to-Play Video Facade',
71 - 'description' => 'Replace YouTube and Vimeo embeds with a poster image and a play button. The player only loads when a visitor clicks it, so a page with embeds no longer pays ~1MB of third-party JavaScript that most visitors never use. Falls back to the normal embed when JavaScript is off.',
71 + 'label' => __( 'Click-to-Play Video Facade', 'xspeed' ),
72 + 'description' => __( 'Replace YouTube and Vimeo embeds — and self-hosted <video> tags that have a poster — with the poster image and a play button. The video only loads when a visitor clicks it, so a page with embeds no longer pays ~1MB of third-party JavaScript, or the full weight of a hosted video file, for visitors who never press play. Autoplaying videos are left alone. Falls back to the normal embed when JavaScript is off.', 'xspeed' ),
72 73 ),
73 74 'lazy_videos' => array(
74 75 'type' => 'bool',
75 76 'default' => true,
76 - 'label' => 'Lazy-load HTML5 Videos',
77 - 'description' => 'Set preload="none" on self-hosted <video> tags. Browsers do not yet support loading="lazy" on video; preload="none" is the closest equivalent.',
77 + 'label' => __( 'Lazy-load HTML5 Videos', 'xspeed' ),
78 + 'description' => __( 'Set preload="none" on self-hosted <video> tags, overriding a player\'s own preload="auto"/"metadata". Autoplaying videos are left alone — they need their bytes regardless. Browsers do not yet support loading="lazy" on video; preload="none" is the closest equivalent.', 'xspeed' ),
78 79 ),
79 80 'eager_first_n' => array(
80 81 'type' => 'int',
81 82 'default' => 1,
@@ -80,23 +81,23 @@
80 81 'type' => 'int',
81 82 'default' => 1,
82 83 'min' => 0,
83 84 'max' => 10,
84 - 'label' => 'Eager-load First N Images',
85 - '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.',
85 + 'label' => __( 'Eager-load First N Images', 'xspeed' ),
86 + '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.', 'xspeed' ),
86 87 ),
87 88 'add_missing_dimensions' => array(
88 89 'type' => 'bool',
89 90 'default' => true,
90 - 'label' => 'Add Missing Image Dimensions',
91 - '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.',
91 + 'label' => __( 'Add Missing Image Dimensions', 'xspeed' ),
92 + '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.', 'xspeed' ),
92 93 ),
93 94 'excluded_images' => array(
94 95 'type' => 'list',
95 96 'default' => array(),
96 97 'item_type' => 'string',
97 - 'label' => 'Excluded Images',
98 - '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.',
98 + 'label' => __( 'Excluded Images', 'xspeed' ),
99 + '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.', 'xspeed' ),
99 100 ),
100 101 );
101 102 }
102 103
@@ -117,8 +118,27 @@
117 118 );
118 119 }
119 120
120 121 public function boot(): void {
122 + /*
123 + * Deferred to `init`. This module reads its own settings to decide
124 + * what to hook, and reading settings builds settings_schema(), whose
125 + * labels are declared through __(). boot() runs on `plugins_loaded`,
126 + * before `after_setup_theme` — the point WordPress 6.7+ treats as the
127 + * earliest safe moment to translate — so doing that here fires
128 + * _load_textdomain_just_in_time on every request AND resolves the
129 + * labels against a domain that is not loaded yet.
130 + *
131 + * Everything below hooks actions that fire after `init`, so running
132 + * one hook later is equivalent.
133 + */
134 + add_action( 'init', array( $this, 'boot_on_init' ) );
135 + }
136 +
137 + /**
138 + * The real boot body — see boot() for why it runs on `init`.
139 + */
140 + public function boot_on_init(): void {
121 141 // Bail entirely on admin / feed / cron / REST — same scope as
122 142 // Minifier. Lazy-loading rendered HTML only matters on real
123 143 // frontend page renders.
124 144 if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
@@ -124,8 +144,14 @@
124 144 if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
125 145 return;
126 146 }
127 147
148 + // Never lazy-load inside a builder editor: the builder measures and
149 + // positions elements it expects to be loaded. (#281)
150 + if ( \XSpeed\Builder_Editor::is_active() ) {
151 + return;
152 + }
153 +
128 154 $opts = $this->get_settings();
129 155 $any_enabled = ! empty( $opts['lazy_images'] )
130 156 || ! empty( $opts['lazy_iframes'] )
131 157 || ! empty( $opts['lazy_videos'] )
@@ -160,8 +186,30 @@
160 186 // after wp_head — and a layout rule that arrives in the footer
161 187 // fixes the gap only after the visitor has already seen it.
162 188 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_facade_style' ) );
163 189 }
190 +
191 + // Same conditional-footer treatment for the autoplay restorer: it is
192 + // only printed on a response that actually deferred one.
193 + if ( ! empty( $opts['lazy_videos'] ) ) {
194 + /*
195 + * HEAD, not footer — and as early as anything can run.
196 + *
197 + * A page-builder video block creates its <video> from its own
198 + * script. Ours has to be listening BEFORE that happens: printed
199 + * in the footer it loaded after the block's script had already
200 + * built the player and started the fetch, so the bytes were
201 + * committed before we could defer them. Measured on a live page,
202 + * our restorer sat ~17KB after the first block script in the
203 + * document, and the videos still downloaded on load.
204 + *
205 + * It costs ~1KB inline and installs only observers, so running it
206 + * early is cheap; a MutationObserver on documentElement catches
207 + * every <video> the moment it is inserted, whichever script did
208 + * the inserting.
209 + */
210 + add_action( 'wp_head', array( $this, 'print_autoplay_script' ), 1 );
211 + }
164 212 }
165 213
166 214 /**
167 215 * Register the facade's layout rule as an inline style on a
@@ -185,8 +233,35 @@
185 233
186 234 wp_print_inline_script_tag( \XSpeed\Video_Facade::facade_script(), array( 'id' => 'xspeed-video-facade' ) );
187 235 }
188 236
237 + /**
238 + * Emit the viewport restorer for deferred AUTOPLAY videos.
239 + *
240 + * Separate from the facade script because the two are independent: a
241 + * page can defer an autoplay hero without any click-to-play facade on
242 + * it, and vice versa. Both are gated on having actually rewritten
243 + * something, so a page with no video ships neither.
244 + */
245 + public function print_autoplay_script(): void {
246 + /*
247 + * Deliberately NOT gated on needs_autoplay_script().
248 + *
249 + * That flag is only meaningful after the content filter has run, and
250 + * this prints in wp_head — long before. The facade script above can
251 + * afford to be conditional because it only has to be present by the
252 + * time a human clicks; this one has to be listening before another
253 + * plugin's script builds a <video> and starts fetching it, which
254 + * happens well before wp_footer.
255 + *
256 + * The cost of being unconditional is ~1KB inline on pages with no
257 + * video, and the script installs observers only — it does no work
258 + * and touches nothing when it finds no autoplay video. That is a
259 + * better trade than missing the one case the feature exists for.
260 + */
261 + wp_print_inline_script_tag( Lazy_Loader::autoplay_script(), array( 'id' => 'xspeed-lazy-autoplay' ) );
262 + }
263 +
189 264 public function cli_commands(): array {
190 265 return array(
191 266 array(
192 267 'name' => 'xspeed lazy',
@@ -191,8 +266,9 @@
191 266 array(
192 267 'name' => 'xspeed lazy',
193 268 'callback' => array( $this, 'cli_handler' ),
194 269 'shortdesc' => 'Show which lazy-load toggles are active.',
270 + 'ai_hint' => 'Which lazy-loading and image optimizations are on (images, iframes, missing width/height)? Use for questions about images loading too early, layout shift (CLS), or offscreen images flagged by PageSpeed.',
195 271 'synopsis' => array(),
196 272 ),
197 273 );
198 274 }
@@ -205,6 +281,14 @@
205 281 $display = (string) $value;
206 282 }
207 283 \WP_CLI::log( sprintf( '%-30s %s', $key, $display ) );
208 284 }
285 + }
286 +
287 + /**
288 + * Lazy has no master switch -- it is on when any of lazy_images /
289 + * lazy_iframes / video_facade / lazy_videos is set. (#363)
290 + */
291 + public function is_active(): ?bool {
292 + return $this->any_bool_flag_on();
209 293 }
210 294 }