PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.6
1.3.6 1.3.5 1.3.4 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 All 32 releases
← All changes | includes/modules/Lazy/LazyModule.php +90 -21 1.2.4 → 1.3.6 View file →
@@ -38,12 +38,12 @@
38 38 public const VERSION = '1.0.0';
39 39
40 40 public function ui_metadata(): array {
41 41 return array(
42 - 'label' => 'Media Optimization',
43 - 'tab_label' => 'Lazy Loading', // its own tab on the Media Optimization 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, and videos load — lazy-loading, missing dimensions, and format optimization.',
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 47 // + AI Suggestions (Pro) as tabs — everything a page loads on one
48 48 // page instead of separate rows (FBS-83633). Fonts is NOT here: it
49 49 // has its own Optimization card (#86).
@@ -55,49 +55,55 @@
55 55 return array(
56 56 'lazy_images' => array(
57 57 'type' => 'bool',
58 58 'default' => true,
59 - 'label' => 'Lazy-load Images',
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.',
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' ),
61 61 ),
62 62 'lazy_iframes' => array(
63 63 'type' => 'bool',
64 64 'default' => true,
65 - 'label' => 'Lazy-load Iframes',
66 - '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' ),
67 67 ),
68 68 'video_facade' => array(
69 69 'type' => 'bool',
70 70 'default' => false,
71 - 'label' => 'Click-to-Play Video Facade',
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.',
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' ),
73 73 ),
74 74 'lazy_videos' => array(
75 75 'type' => 'bool',
76 76 'default' => true,
77 - 'label' => 'Lazy-load HTML5 Videos',
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.',
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' ),
79 79 ),
80 + 'lazy_background_images' => array(
81 + 'type' => 'bool',
82 + 'default' => false,
83 + 'label' => __( 'Lazy-load Background Images', 'xspeed' ),
84 + 'description' => __( 'Hold back background images set in an element\'s inline style (a Cover or Group block, a page-builder section) until the element is near the screen. Uses a small script; visitors without JavaScript get every background as usual. The first N backgrounds on the page load straight away, like images. Backgrounds set in a stylesheet are not affected.', 'xspeed' ),
85 + ),
80 86 'eager_first_n' => array(
81 87 'type' => 'int',
82 88 'default' => 1,
83 89 'min' => 0,
84 90 'max' => 10,
85 - 'label' => 'Eager-load First N Images',
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.',
91 + 'label' => __( 'Eager-load First N Images', 'xspeed' ),
92 + '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. Lazy-loaded background images use the same number.', 'xspeed' ),
87 93 ),
88 94 'add_missing_dimensions' => array(
89 95 'type' => 'bool',
90 96 'default' => true,
91 - 'label' => 'Add Missing Image Dimensions',
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.',
97 + 'label' => __( 'Add Missing Image Dimensions', 'xspeed' ),
98 + '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' ),
93 99 ),
94 100 'excluded_images' => array(
95 101 'type' => 'list',
96 102 'default' => array(),
97 103 'item_type' => 'string',
98 - 'label' => 'Excluded Images',
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.',
104 + 'label' => __( 'Excluded Images', 'xspeed' ),
105 + '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' ),
100 106 ),
101 107 );
102 108 }
103 109
@@ -118,8 +124,27 @@
118 124 );
119 125 }
120 126
121 127 public function boot(): void {
128 + /*
129 + * Deferred to `init`. This module reads its own settings to decide
130 + * what to hook, and reading settings builds settings_schema(), whose
131 + * labels are declared through __(). boot() runs on `plugins_loaded`,
132 + * before `after_setup_theme` — the point WordPress 6.7+ treats as the
133 + * earliest safe moment to translate — so doing that here fires
134 + * _load_textdomain_just_in_time on every request AND resolves the
135 + * labels against a domain that is not loaded yet.
136 + *
137 + * Everything below hooks actions that fire after `init`, so running
138 + * one hook later is equivalent.
139 + */
140 + add_action( 'init', array( $this, 'boot_on_init' ) );
141 + }
142 +
143 + /**
144 + * The real boot body — see boot() for why it runs on `init`.
145 + */
146 + public function boot_on_init(): void {
122 147 // Bail entirely on admin / feed / cron / REST — same scope as
123 148 // Minifier. Lazy-loading rendered HTML only matters on real
124 149 // frontend page renders.
125 150 if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
@@ -136,8 +161,9 @@
136 161 $any_enabled = ! empty( $opts['lazy_images'] )
137 162 || ! empty( $opts['lazy_iframes'] )
138 163 || ! empty( $opts['lazy_videos'] )
139 164 || ! empty( $opts['video_facade'] )
165 + || ! empty( $opts['lazy_background_images'] )
140 166 || ! empty( $opts['add_missing_dimensions'] );
141 167 if ( ! $any_enabled ) {
142 168 return;
143 169 }
@@ -166,10 +192,23 @@
166 192 // renders isn't known until the content filter has run — long
167 193 // after wp_head — and a layout rule that arrives in the footer
168 194 // fixes the gap only after the visitor has already seen it.
169 195 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_facade_style' ) );
196 + // Embeds that never touch the HTML: a builder widget builds its
197 + // YouTube iframe from script, so the buffer pass has nothing to
198 + // rewrite. Head, priority 1, for the same reason as the autoplay
199 + // restorer below — it must be listening before the widget's
200 + // script sets a src and commits the fetch.
201 + add_action( 'wp_head', array( $this, 'print_observer_script' ), 1 );
170 202 }
171 203
204 + // Head, because the rule that holds a background back has to apply
205 + // before first paint, or the browser has already requested it.
206 + if ( ! empty( $opts['lazy_background_images'] ) ) {
207 + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_background_style' ) );
208 + add_action( 'wp_head', array( $this, 'print_background_script' ), 1 );
209 + }
210 +
172 211 // Same conditional-footer treatment for the autoplay restorer: it is
173 212 // only printed on a response that actually deferred one.
174 213 if ( ! empty( $opts['lazy_videos'] ) ) {
175 214 /*
@@ -202,18 +241,48 @@
202 241 wp_enqueue_style( 'xspeed-video-facade' );
203 242 wp_add_inline_style( 'xspeed-video-facade', \XSpeed\Video_Facade::facade_style() );
204 243 }
205 244
245 + public function enqueue_background_style(): void {
246 + wp_register_style( 'xspeed-lazy-bg', false, array(), XSPEED_VERSION );
247 + wp_enqueue_style( 'xspeed-lazy-bg' );
248 + wp_add_inline_style( 'xspeed-lazy-bg', Lazy_Loader::background_style() );
249 + }
250 +
206 251 /**
252 + * data-xs-nodelay: Delay JS would otherwise hold this back until the
253 + * first interaction, and every held background would stay blank.
254 + */
255 + public function print_background_script(): void {
256 + wp_print_inline_script_tag(
257 + Lazy_Loader::background_script(),
258 + array(
259 + 'id' => 'xspeed-lazy-bg',
260 + 'data-xs-nodelay' => true,
261 + )
262 + );
263 + }
264 +
265 + /**
207 266 * Emit the click-to-play handler inline. Inline (not enqueued) because
208 267 * it is ~400 bytes — a separate request would cost more than the code.
209 268 */
210 269 public function print_facade_script(): void {
211 - if ( ! Lazy_Loader::facade_used() ) {
212 - return;
213 - }
270 + // No longer gated on facade_used(): the observer script can build a
271 + // facade for a JS-injected embed on a page where the server pass
272 + // rendered none, and a facade without its click handler is a play
273 + // button that plays nothing. ~400 bytes on facade-less pages is the
274 + // cost of never shipping that.
275 + wp_print_inline_script_tag( \XSpeed\Video_Facade::facade_script(), array( 'id' => 'xspeed-video-facade' ) );
276 + }
214 277
215 - wp_print_inline_script_tag( \XSpeed\Video_Facade::facade_script(), array( 'id' => 'xspeed-video-facade' ) );
278 + /**
279 + * Emit the interceptor for JS-injected embeds (see
280 + * Video_Facade::observer_script() for the mechanism and why the
281 + * footer is too late).
282 + */
283 + public function print_observer_script(): void {
284 + wp_print_inline_script_tag( \XSpeed\Video_Facade::observer_script(), array( 'id' => 'xspeed-video-facade-observer' ) );
216 285 }
217 286
218 287 /**
219 288 * Emit the viewport restorer for deferred AUTOPLAY videos.