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/Minify/MinifyModule.php +152 -24 1.0.2 → 1.3.6 View file →
@@ -16,9 +16,12 @@
16 16 declare(strict_types=1);
17 17
18 18 namespace XSpeed\Modules\Minify;
19 19
20 +defined( 'ABSPATH' ) || exit;
21 +
20 22 use XSpeed\Minifier as LegacyMinifier;
23 +use XSpeed\Minify_Filters;
21 24 use XSpeed\Module;
22 25 use XSpeed\Settings_Manager;
23 26
24 27 final class MinifyModule extends Module {
@@ -28,11 +31,16 @@
28 31 public const VERSION = '1.2.0';
29 32
30 33 public function ui_metadata(): array {
31 34 return array(
32 - 'label' => 'Minify',
33 - 'icon' => 'Wand2',
34 - 'description' => 'Strip whitespace and rewrite enqueued CSS / JS.',
35 + 'label' => __( 'CSS & JavaScript', 'xspeed' ),
36 + 'tab_label' => __( 'Minify', 'xspeed' ), // its own tab on the CSS & JavaScript page
37 + 'icon' => 'Wand2',
38 + 'description' => __( 'Strip whitespace and rewrite enqueued CSS / JS.', 'xspeed' ),
39 + // Host page: Minify (this module) / Critical CSS (Pro) / Unused
40 + // CSS (Pro) as tabs — the three CSS/JS optimizations live on one
41 + // page instead of three sidebar rows (FBS-83633).
42 + 'custom_panel' => 'CssJsPanel',
35 43 );
36 44 }
37 45
38 46 public function settings_schema(): array {
@@ -39,65 +47,108 @@
39 47 return array(
40 48 'minify_html' => array(
41 49 'type' => 'bool',
42 50 'default' => false,
43 - 'label' => 'Minify HTML',
44 - 'description' => 'Strip whitespace and comments from HTML output. Safe on most themes.',
51 + 'label' => __( 'Minify HTML', 'xspeed' ),
52 + // Names the logged-out caveat up front: minification runs on the
53 + // request that WRITES a cache entry, and should_cache() refuses
54 + // logged-in requests — so "view source while logged in" shows
55 + // un-minified HTML and reads as the feature being broken. (#2)
56 + 'description' => __( 'Strip whitespace and comments from HTML output, including inline <style> and <script> blocks. Safe on most themes. Applies to cached (logged-out) responses — view the page in a private window to see the result.', 'xspeed' ),
45 57 ),
46 58 'minify_css' => array(
47 59 'type' => 'bool',
48 60 'default' => false,
49 - 'label' => 'Minify CSS',
50 - 'description' => 'Compress and rewrite enqueued local stylesheets. External CSS is left untouched.',
61 + 'label' => __( 'Minify CSS', 'xspeed' ),
62 + 'description' => __( 'Compress and rewrite enqueued local stylesheets. External CSS is left untouched.', 'xspeed' ),
51 63 ),
52 64 'minify_js' => array(
53 65 'type' => 'bool',
54 66 'default' => false,
55 - 'label' => 'Minify JavaScript',
56 - 'description' => 'Compress enqueued local scripts. Disable if you hit script-loading conflicts on the frontend.',
67 + 'label' => __( 'Minify JavaScript', 'xspeed' ),
68 + 'description' => __( 'Compress enqueued local scripts. Disable if you hit script-loading conflicts on the frontend.', 'xspeed' ),
57 69 ),
58 70 'defer_js' => array(
59 71 'type' => 'bool',
60 72 'default' => false,
61 - 'label' => 'Defer JavaScript',
62 - 'description' => 'Add defer="defer" to enqueued script tags so they execute after HTML parsing. jQuery + its hard dependencies are skipped automatically.',
73 + 'label' => __( 'Defer JavaScript', 'xspeed' ),
74 + 'description' => __( 'Add defer="defer" to enqueued script tags so they execute after HTML parsing. jQuery + its hard dependencies are skipped automatically.', 'xspeed' ),
63 75 ),
64 76 'delay_js' => array(
65 77 'type' => 'bool',
66 78 'default' => false,
67 - 'label' => 'Delay JavaScript Until Interaction',
68 - 'description' => 'Postpone script loading until the visitor scrolls, moves the mouse, taps, or presses a key. Drastically improves first paint on script-heavy pages; can break above-the-fold scripted UI — test before leaving on.',
79 + 'label' => __( 'Delay JavaScript Until Interaction', 'xspeed' ),
80 + 'description' => __( 'Postpone script loading until the visitor scrolls, moves the mouse, taps, or presses a key. Drastically improves first paint on script-heavy pages; can break above-the-fold scripted UI — test before leaving on. Cookie-consent banners (CookieYes, Complianz, Cookiebot, NotificationX, Cookie Notice, Borlabs, Real Cookie Banner and others) are not delayed, so the visitor is still asked before anything else runs. To delay one on purpose, name it in Delay Only These Scripts.', 'xspeed' ),
69 81 ),
70 82 'async_css' => array(
71 83 'type' => 'bool',
72 84 'default' => false,
73 - 'label' => 'Load CSS Asynchronously',
74 - 'description' => 'Rewrite stylesheet link tags to load non-blocking via the print-then-all pattern. Pairs well with critical-CSS workflows; can cause a flash of unstyled content if the theme has no critical CSS.',
85 + 'label' => __( 'Load CSS Asynchronously', 'xspeed' ),
86 + 'description' => __( 'Rewrite stylesheet link tags to load non-blocking via the print-then-all pattern. Pairs well with critical-CSS workflows; can cause a flash of unstyled content if the theme has no critical CSS.', 'xspeed' ),
75 87 ),
76 88 'remove_query_strings' => array(
77 89 'type' => 'bool',
78 90 'default' => false,
79 - 'label' => 'Remove Asset Query Strings',
80 - 'description' => 'Strip ?ver=X.Y from enqueued CSS / JS URLs. Some CDN caches and proxies cache better when query strings are absent.',
91 + 'label' => __( 'Remove Asset Query Strings', 'xspeed' ),
92 + 'description' => __( 'Strip ?ver=X.Y from enqueued CSS / JS URLs. Some CDN caches and proxies cache better when query strings are absent. Files under wp-content/uploads keep their version — page builders and consent plugins rewrite generated CSS there in place, and ?ver is what tells browsers to refetch it. Plugin, theme and core assets are still stripped: with Browser Cache on, an update reaches returning visitors only when their browser cache expires.', 'xspeed' ),
81 93 ),
82 94 'defer_js_excluded' => array(
83 95 'type' => 'list',
84 96 'default' => array( 'jquery-core', 'jquery-migrate' ),
85 97 'item_type' => 'string',
86 - 'label' => 'Defer / Delay Exclusions',
87 - 'description' => 'Script handles OR URL substrings that skip defer + delay. Defaults exclude jQuery (most themes depend on it being available synchronously). One per line.',
98 + 'label' => __( 'Defer / Delay Exclusions', 'xspeed' ),
99 + 'description' => __( 'Script handles OR URL substrings that skip defer + delay. Defaults exclude jQuery (most themes depend on it being available synchronously). Cookie-consent banners are skipped without being listed here. An entry here also wins over the same script in Delay Only These Scripts. One per line.', 'xspeed' ),
100 + // Only relevant once defer OR delay is on — the exclusion list
101 + // governs both. Uses the `any` (OR) dependency form. (FBS-82227)
102 + 'dependsOn' => array(
103 + 'any' => array(
104 + array( 'field' => 'defer_js' ),
105 + array( 'field' => 'delay_js' ),
106 + ),
107 + ),
88 108 ),
109 + 'delay_js_targets' => array(
110 + 'type' => 'list',
111 + 'default' => array(),
112 + 'item_type' => 'string',
113 + 'label' => __( 'Delay Only These Scripts', 'xspeed' ),
114 + 'description' => __( 'Script handles OR URL substrings. When non-empty, matching scripts are delayed, plus the known third-party tags xSpeed recognises on its own (analytics, tag managers, chat widgets, review embeds, error trackers) — so a heavy vendor script is postponed even when it is not listed here. Leave empty to delay all scripts (minus the exclusions above). Handles are the more reliable selector — a URL substring has to match the script\'s original URL, and minification rewrites that to a hashed cache path. Cookie-consent banners are only delayed when an entry names one, for example notificationx or cookiebot. A broad entry such as /plugins/ does not delay them. One per line.', 'xspeed' ),
115 + 'info_title' => __( 'Consent banners and Delay JS', 'xspeed' ),
116 + 'info' => sprintf(
117 + /* translators: %s: comma-separated list of consent plugins, each followed by the word to type in parentheses. */
118 + __( 'These consent banners load straight away, even with Delay JS on: %s. To delay one on purpose, add the word in parentheses, or the script handle, to this list. An entry that only matches part of the address, such as /plugins/ or .js, never delays a banner, and an entry in Defer / Delay Exclusions always wins.', 'xspeed' ),
119 + implode( ', ', Minify_Filters::consent_manager_labels() )
120 + ),
121 + 'dependsOn' => array( 'field' => 'delay_js' ),
122 + ),
123 + 'delay_js_smart' => array(
124 + 'type' => 'bool',
125 + 'default' => false,
126 + 'label' => __( 'Smart Delay (Carry Inline Snippets)', 'xspeed' ),
127 + 'description' => __( 'Normally a script is left running at load when inline code on the page depends on it — on builder pages that keeps most scripts eager even with Delay JavaScript on. Smart Delay postpones those scripts anyway and parks their inline snippets with them; everything replays in page order on the first interaction. The most aggressive setting on this page: test menus, sliders, and toggles before leaving it on. Exclusions above still win, and consent banners still run first.', 'xspeed' ),
128 + 'dependsOn' => array( 'field' => 'delay_js' ),
129 + ),
130 + 'delay_js_timeout' => array(
131 + 'type' => 'int',
132 + 'default' => 8000,
133 + 'min' => 0,
134 + 'max' => 60000,
135 + 'label' => __( 'Delay Failsafe Timeout (ms)', 'xspeed' ),
136 + 'unit' => 'ms',
137 + 'description' => __( 'Load delayed scripts automatically after this many milliseconds when the visitor never interacts. Set to 0 for interaction-only, with no timer: a timer that fires inside a lab tool\'s measurement window loads the "delayed" scripts anyway and inflates the reported TTI. Keep a non-zero value if a delayed script must eventually run for visitors who never scroll, tap, or type.', 'xspeed' ),
138 + 'dependsOn' => array( 'field' => 'delay_js' ),
139 + ),
89 140 'combine_css' => array(
90 141 'type' => 'bool',
91 142 'default' => false,
92 - 'label' => 'Combine CSS Files',
93 - 'description' => 'Concatenate enqueued local stylesheets into a single file (with @import and url(…) paths resolved). External CSS is left alone. Pairs poorly with HTTP/2 push — only enable on HTTP/1.1 hosts.',
143 + 'label' => __( 'Combine CSS Files', 'xspeed' ),
144 + 'description' => __( 'Concatenate enqueued local stylesheets into a single file (with @import and url(…) paths resolved). External CSS is left alone. Pairs poorly with HTTP/2 push — only enable on HTTP/1.1 hosts.', 'xspeed' ),
94 145 ),
95 146 'combine_js' => array(
96 147 'type' => 'bool',
97 148 'default' => false,
98 - 'label' => 'Combine JavaScript Files',
99 - 'description' => 'Concatenate enqueued local scripts into a single file. External scripts + scripts marked async / deferred are left alone. Disable if you hit dependency-order issues; the combiner respects WordPress enqueue order but inline scripts attached via wp_add_inline_script can shift behavior.',
149 + 'label' => __( 'Combine JavaScript Files', 'xspeed' ),
150 + 'description' => __( 'Concatenate enqueued local scripts into a single file. External scripts + scripts marked async / deferred are left alone. Disable if you hit dependency-order issues; the combiner respects WordPress enqueue order but inline scripts attached via wp_add_inline_script can shift behavior.', 'xspeed' ),
100 151 ),
101 152 );
102 153 }
103 154
@@ -157,8 +208,9 @@
157 208 array(
158 209 'name' => 'xspeed minify',
159 210 'callback' => array( $this, 'cli_handler' ),
160 211 'shortdesc' => 'Inspect or purge xSpeed minify cache.',
212 + 'ai_hint' => 'Which CSS/JS optimizations are active (minify, combine, defer, delay, async)? Use for questions about render-blocking resources, unminified assets in PageSpeed, or when JavaScript broke after enabling optimizations.',
161 213 'synopsis' => array(
162 214 array(
163 215 'type' => 'positional',
164 216 'name' => 'action',
@@ -178,8 +230,26 @@
178 230 * 2. Instantiate the v1 Minifier engine; it now reads from
179 231 * Settings_Manager::get('minify') via its updated read path.
180 232 */
181 233 public function boot(): void {
234 + /*
235 + * Deferred to `init`: both calls below read this module's settings,
236 + * which builds settings_schema(), whose labels go through __().
237 + * boot() runs on `plugins_loaded`, before `after_setup_theme` — the
238 + * earliest point WordPress 6.7+ considers safe to translate — so doing
239 + * it here fires _load_textdomain_just_in_time on every request and
240 + * resolves those labels against an unloaded domain.
241 + *
242 + * Every filter LegacyMinifier registers fires after `init`, so running
243 + * one hook later is equivalent.
244 + */
245 + add_action( 'init', array( $this, 'boot_on_init' ) );
246 + }
247 +
248 + /**
249 + * The real boot body — see boot() for why it runs on `init`.
250 + */
251 + public function boot_on_init(): void {
182 252 $this->seed_from_legacy_if_needed();
183 253 new LegacyMinifier();
184 254 }
185 255
@@ -188,8 +258,45 @@
188 258 // run more than once.
189 259 $this->seed_from_legacy_if_needed();
190 260 }
191 261
262 + /**
263 + * Say so when HTML minification is switched on but suppressed.
264 + *
265 + * `Minifier::skip_reason()` was consulted only by `wp xspeed minify status`
266 + * — the dashboard read "on" while `minify_html()` returned its input
267 + * untouched, so the feature looked broken rather than paused. A field
268 + * report showed a live site with `minify_html: on` and 3,856 indented lines
269 + * delivered, and nothing anywhere explaining the contradiction. (#2)
270 + *
271 + * @return array<int,array<string,mixed>>
272 + */
273 + public function ui_notices(): array {
274 + $opts = $this->get_settings();
275 + if ( empty( $opts['minify_html'] ) ) {
276 + return array();
277 + }
278 +
279 + $reason = LegacyMinifier::skip_reason();
280 + if ( '' === $reason ) {
281 + return array();
282 + }
283 +
284 + // Two different causes, two different fixes — naming the wrong one
285 + // sends the user hunting in the wrong file.
286 + $body = 'wp_debug' === $reason
287 + ? __( 'HTML minification is paused because WP_DEBUG is enabled in wp-config.php. Readable HTML is usually what you want while debugging, so xSpeed leaves the markup alone. Cached pages are served un-minified until WP_DEBUG is turned off.', 'xspeed' )
288 + : __( 'HTML minification is paused because a plugin or theme is returning true from the xspeed_skip_minify filter. Cached pages are served un-minified until that filter stops suppressing it.', 'xspeed' );
289 +
290 + return array(
291 + array(
292 + 'tone' => 'info',
293 + 'title' => __( 'HTML minification is on but currently paused', 'xspeed' ),
294 + 'body' => $body,
295 + ),
296 + );
297 + }
298 +
192 299 private function seed_from_legacy_if_needed(): void {
193 300 $existing = get_option( 'xspeed_module_minify', null );
194 301 if ( null !== $existing ) {
195 302 return;
@@ -217,9 +324,21 @@
217 324 $action = $args[0] ?? 'status';
218 325
219 326 if ( 'status' === $action ) {
220 327 $opts = Settings_Manager::get( self::SLUG );
221 - \WP_CLI::log( sprintf( 'minify_html: %s', $opts['minify_html'] ? 'on' : 'off' ) );
328 + // A bare "on" is a lie when the skip guard is active: the
329 + // setting is stored, but Minifier::minify_html() returns its
330 + // input untouched and the delivered HTML is unchanged. Say so
331 + // on the same line, so the contradiction can never be read as
332 + // "minify is broken".
333 + $skip = \XSpeed\Minifier::skip_reason();
334 + $html_state = $opts['minify_html'] ? 'on' : 'off';
335 + if ( $opts['minify_html'] && '' !== $skip ) {
336 + $html_state .= ( 'wp_debug' === $skip )
337 + ? ' (NOT APPLIED — WP_DEBUG is enabled; set WP_DEBUG to false to minify HTML)'
338 + : ' (NOT APPLIED — suppressed by the xspeed_skip_minify filter)';
339 + }
340 + \WP_CLI::log( sprintf( 'minify_html: %s', $html_state ) );
222 341 \WP_CLI::log( sprintf( 'minify_css : %s', $opts['minify_css'] ? 'on' : 'off' ) );
223 342 \WP_CLI::log( sprintf( 'minify_js : %s', $opts['minify_js'] ? 'on' : 'off' ) );
224 343 return;
225 344 }
@@ -230,6 +349,15 @@
230 349 return;
231 350 }
232 351
233 352 \WP_CLI::error( "Unknown action: $action" );
353 + }
354 +
355 + /**
356 + * Minify has no master switch -- it is on when any of minify_html /
357 + * minify_css / minify_js / defer_js / delay_js / async_css /
358 + * remove_query_strings is set. (#363)
359 + */
360 + public function is_active(): ?bool {
361 + return $this->any_bool_flag_on();
234 362 }
235 363 }