PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.1.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.1.0
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.1.0, at includes/modules/Lazy/LazyModule.php

168 lines 6.4 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 defined( 'ABSPATH' ) || exit;
30
31 use XSpeed\Lazy_Loader;
32 use XSpeed\Module;
33
34 final class LazyModule extends Module {
35
36 public const SLUG = 'lazy';
37 public const TIER = self::TIER_FREE;
38 public const VERSION = '1.0.0';
39
40 public function ui_metadata(): array {
41 return array(
42 'label' => 'Media & Fonts',
43 'tab_label' => 'Lazy Loading', // its own tab on the Media & Fonts page
44 'icon' => 'Image',
45 'description' => 'Control how images, iframes, videos, and web fonts load — lazy-loading, format optimization, and font display.',
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).
49 'custom_panel' => 'MediaPanel',
50 );
51 }
52
53 public function settings_schema(): array {
54 return array(
55 'lazy_images' => array(
56 'type' => 'bool',
57 '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.',
60 ),
61 'lazy_iframes' => array(
62 'type' => 'bool',
63 '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.',
66 ),
67 'lazy_videos' => array(
68 'type' => 'bool',
69 'default' => true,
70 'label' => 'Lazy-load HTML5 Videos',
71 'description' => 'Set preload="none" on self-hosted <video> tags. Browsers do not yet support loading="lazy" on video; preload="none" is the closest equivalent.',
72 ),
73 'eager_first_n' => array(
74 'type' => 'int',
75 'default' => 1,
76 'min' => 0,
77 'max' => 10,
78 'label' => 'Eager-load First N Images',
79 '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.',
80 ),
81 'add_missing_dimensions' => array(
82 'type' => 'bool',
83 'default' => true,
84 'label' => 'Add Missing Image Dimensions',
85 '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.',
86 ),
87 'excluded_images' => array(
88 'type' => 'list',
89 'default' => array(),
90 'item_type' => 'string',
91 'label' => 'Excluded Images',
92 '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.',
93 ),
94 );
95 }
96
97 public function conflicts(): array {
98 return array(
99 array(
100 'plugin' => 'wp-smushit/wp-smush.php',
101 'feature' => 'images.lazyload',
102 'strategy' => \XSpeed\Conflict_Registry::STRATEGY_WARN,
103 'reason' => 'Smush also offers lazy-loading; running both can cause double-rewriting.',
104 ),
105 array(
106 'plugin' => 'a3-lazy-load/a3-lazy-load.php',
107 'feature' => 'images.lazyload',
108 'strategy' => \XSpeed\Conflict_Registry::STRATEGY_REFUSE,
109 'reason' => 'a3 Lazy Load is a dedicated lazy-load plugin; disable it before enabling xSpeed lazy-load.',
110 ),
111 );
112 }
113
114 public function boot(): void {
115 // Bail entirely on admin / feed / cron / REST — same scope as
116 // Minifier. Lazy-loading rendered HTML only matters on real
117 // frontend page renders.
118 if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
119 return;
120 }
121
122 $opts = $this->get_settings();
123 $any_enabled = ! empty( $opts['lazy_images'] )
124 || ! empty( $opts['lazy_iframes'] )
125 || ! empty( $opts['lazy_videos'] )
126 || ! empty( $opts['add_missing_dimensions'] );
127 if ( ! $any_enabled ) {
128 return;
129 }
130
131 // Reset the eager-load budget once per page render, before any
132 // content filter runs, so the "first N images eager" budget is
133 // shared across the featured image + content + avatars rather than
134 // restarting on every filter pass. (FBS-82172 Bug 1)
135 add_action( 'template_redirect', array( Lazy_Loader::class, 'reset_state' ) );
136
137 // Late priority so the_content runs after every other filter
138 // (shortcodes, do_blocks, embeds). Avoids rewriting tags that
139 // haven't been generated yet.
140 add_filter( 'the_content', array( Lazy_Loader::class, 'process_html' ), 999 );
141 add_filter( 'post_thumbnail_html', array( Lazy_Loader::class, 'process_html' ), 999 );
142 add_filter( 'get_avatar', array( Lazy_Loader::class, 'process_html' ), 999 );
143 add_filter( 'widget_text_content', array( Lazy_Loader::class, 'process_html' ), 999 );
144 }
145
146 public function cli_commands(): array {
147 return array(
148 array(
149 'name' => 'xspeed lazy',
150 'callback' => array( $this, 'cli_handler' ),
151 'shortdesc' => 'Show which lazy-load toggles are active.',
152 'synopsis' => array(),
153 ),
154 );
155 }
156
157 public function cli_handler( array $args, array $assoc ): void {
158 $opts = $this->get_settings();
159 foreach ( $opts as $key => $value ) {
160 $display = is_array( $value ) ? implode( ',', $value ) : ( $value ? 'on' : ( is_numeric( $value ) ? (string) $value : 'off' ) );
161 if ( is_int( $value ) ) {
162 $display = (string) $value;
163 }
164 \WP_CLI::log( sprintf( '%-30s %s', $key, $display ) );
165 }
166 }
167 }
168