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/Minify/MinifyModule.php +138 -24 1.0.21.3.2 View file →
@@ -16,8 +16,10 @@
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;
21 23 use XSpeed\Module;
22 24 use XSpeed\Settings_Manager;
23 25
@@ -28,11 +30,16 @@
28 30 public const VERSION = '1.2.0';
29 31
30 32 public function ui_metadata(): array {
31 33 return array(
32 - 'label' => 'Minify',
33 - 'icon' => 'Wand2',
34 - 'description' => 'Strip whitespace and rewrite enqueued CSS / JS.',
34 + 'label' => __( 'CSS & JavaScript', 'xspeed' ),
35 + 'tab_label' => __( 'Minify', 'xspeed' ), // its own tab on the CSS & JavaScript page
36 + 'icon' => 'Wand2',
37 + 'description' => __( 'Strip whitespace and rewrite enqueued CSS / JS.', 'xspeed' ),
38 + // Host page: Minify (this module) / Critical CSS (Pro) / Unused
39 + // CSS (Pro) as tabs — the three CSS/JS optimizations live on one
40 + // page instead of three sidebar rows (FBS-83633).
41 + 'custom_panel' => 'CssJsPanel',
35 42 );
36 43 }
37 44
38 45 public function settings_schema(): array {
@@ -39,65 +46,95 @@
39 46 return array(
40 47 'minify_html' => array(
41 48 'type' => 'bool',
42 49 'default' => false,
43 - 'label' => 'Minify HTML',
44 - 'description' => 'Strip whitespace and comments from HTML output. Safe on most themes.',
50 + 'label' => __( 'Minify HTML', 'xspeed' ),
51 + // Names the logged-out caveat up front: minification runs on the
52 + // request that WRITES a cache entry, and should_cache() refuses
53 + // logged-in requests — so "view source while logged in" shows
54 + // un-minified HTML and reads as the feature being broken. (#2)
55 + '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 56 ),
46 57 'minify_css' => array(
47 58 'type' => 'bool',
48 59 'default' => false,
49 - 'label' => 'Minify CSS',
50 - 'description' => 'Compress and rewrite enqueued local stylesheets. External CSS is left untouched.',
60 + 'label' => __( 'Minify CSS', 'xspeed' ),
61 + 'description' => __( 'Compress and rewrite enqueued local stylesheets. External CSS is left untouched.', 'xspeed' ),
51 62 ),
52 63 'minify_js' => array(
53 64 'type' => 'bool',
54 65 'default' => false,
55 - 'label' => 'Minify JavaScript',
56 - 'description' => 'Compress enqueued local scripts. Disable if you hit script-loading conflicts on the frontend.',
66 + 'label' => __( 'Minify JavaScript', 'xspeed' ),
67 + 'description' => __( 'Compress enqueued local scripts. Disable if you hit script-loading conflicts on the frontend.', 'xspeed' ),
57 68 ),
58 69 'defer_js' => array(
59 70 'type' => 'bool',
60 71 '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.',
72 + 'label' => __( 'Defer JavaScript', 'xspeed' ),
73 + 'description' => __( 'Add defer="defer" to enqueued script tags so they execute after HTML parsing. jQuery + its hard dependencies are skipped automatically.', 'xspeed' ),
63 74 ),
64 75 'delay_js' => array(
65 76 'type' => 'bool',
66 77 '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.',
78 + 'label' => __( 'Delay JavaScript Until Interaction', 'xspeed' ),
79 + '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.', 'xspeed' ),
69 80 ),
70 81 'async_css' => array(
71 82 'type' => 'bool',
72 83 '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.',
84 + 'label' => __( 'Load CSS Asynchronously', 'xspeed' ),
85 + '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 86 ),
76 87 'remove_query_strings' => array(
77 88 'type' => 'bool',
78 89 '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.',
90 + 'label' => __( 'Remove Asset Query Strings', 'xspeed' ),
91 + '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 92 ),
82 93 'defer_js_excluded' => array(
83 94 'type' => 'list',
84 95 'default' => array( 'jquery-core', 'jquery-migrate' ),
85 96 '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.',
97 + 'label' => __( 'Defer / Delay Exclusions', 'xspeed' ),
98 + 'description' => __( 'Script handles OR URL substrings that skip defer + delay. Defaults exclude jQuery (most themes depend on it being available synchronously). One per line.', 'xspeed' ),
99 + // Only relevant once defer OR delay is on — the exclusion list
100 + // governs both. Uses the `any` (OR) dependency form. (FBS-82227)
101 + 'dependsOn' => array(
102 + 'any' => array(
103 + array( 'field' => 'defer_js' ),
104 + array( 'field' => 'delay_js' ),
105 + ),
106 + ),
88 107 ),
108 + 'delay_js_targets' => array(
109 + 'type' => 'list',
110 + 'default' => array(),
111 + 'item_type' => 'string',
112 + 'label' => __( 'Delay Only These Scripts', 'xspeed' ),
113 + '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. One per line.', 'xspeed' ),
114 + 'dependsOn' => array( 'field' => 'delay_js' ),
115 + ),
116 + 'delay_js_timeout' => array(
117 + 'type' => 'int',
118 + 'default' => 8000,
119 + 'min' => 0,
120 + 'max' => 60000,
121 + 'label' => __( 'Delay Failsafe Timeout (ms)', 'xspeed' ),
122 + 'unit' => 'ms',
123 + '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' ),
124 + 'dependsOn' => array( 'field' => 'delay_js' ),
125 + ),
89 126 'combine_css' => array(
90 127 'type' => 'bool',
91 128 '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.',
129 + 'label' => __( 'Combine CSS Files', 'xspeed' ),
130 + '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 131 ),
95 132 'combine_js' => array(
96 133 'type' => 'bool',
97 134 '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.',
135 + 'label' => __( 'Combine JavaScript Files', 'xspeed' ),
136 + '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 137 ),
101 138 );
102 139 }
103 140
@@ -157,8 +194,9 @@
157 194 array(
158 195 'name' => 'xspeed minify',
159 196 'callback' => array( $this, 'cli_handler' ),
160 197 'shortdesc' => 'Inspect or purge xSpeed minify cache.',
198 + '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 199 'synopsis' => array(
162 200 array(
163 201 'type' => 'positional',
164 202 'name' => 'action',
@@ -178,8 +216,26 @@
178 216 * 2. Instantiate the v1 Minifier engine; it now reads from
179 217 * Settings_Manager::get('minify') via its updated read path.
180 218 */
181 219 public function boot(): void {
220 + /*
221 + * Deferred to `init`: both calls below read this module's settings,
222 + * which builds settings_schema(), whose labels go through __().
223 + * boot() runs on `plugins_loaded`, before `after_setup_theme` — the
224 + * earliest point WordPress 6.7+ considers safe to translate — so doing
225 + * it here fires _load_textdomain_just_in_time on every request and
226 + * resolves those labels against an unloaded domain.
227 + *
228 + * Every filter LegacyMinifier registers fires after `init`, so running
229 + * one hook later is equivalent.
230 + */
231 + add_action( 'init', array( $this, 'boot_on_init' ) );
232 + }
233 +
234 + /**
235 + * The real boot body — see boot() for why it runs on `init`.
236 + */
237 + public function boot_on_init(): void {
182 238 $this->seed_from_legacy_if_needed();
183 239 new LegacyMinifier();
184 240 }
185 241
@@ -188,8 +244,45 @@
188 244 // run more than once.
189 245 $this->seed_from_legacy_if_needed();
190 246 }
191 247
248 + /**
249 + * Say so when HTML minification is switched on but suppressed.
250 + *
251 + * `Minifier::skip_reason()` was consulted only by `wp xspeed minify status`
252 + * — the dashboard read "on" while `minify_html()` returned its input
253 + * untouched, so the feature looked broken rather than paused. A field
254 + * report showed a live site with `minify_html: on` and 3,856 indented lines
255 + * delivered, and nothing anywhere explaining the contradiction. (#2)
256 + *
257 + * @return array<int,array<string,mixed>>
258 + */
259 + public function ui_notices(): array {
260 + $opts = $this->get_settings();
261 + if ( empty( $opts['minify_html'] ) ) {
262 + return array();
263 + }
264 +
265 + $reason = LegacyMinifier::skip_reason();
266 + if ( '' === $reason ) {
267 + return array();
268 + }
269 +
270 + // Two different causes, two different fixes — naming the wrong one
271 + // sends the user hunting in the wrong file.
272 + $body = 'wp_debug' === $reason
273 + ? __( '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' )
274 + : __( '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' );
275 +
276 + return array(
277 + array(
278 + 'tone' => 'info',
279 + 'title' => __( 'HTML minification is on but currently paused', 'xspeed' ),
280 + 'body' => $body,
281 + ),
282 + );
283 + }
284 +
192 285 private function seed_from_legacy_if_needed(): void {
193 286 $existing = get_option( 'xspeed_module_minify', null );
194 287 if ( null !== $existing ) {
195 288 return;
@@ -217,9 +310,21 @@
217 310 $action = $args[0] ?? 'status';
218 311
219 312 if ( 'status' === $action ) {
220 313 $opts = Settings_Manager::get( self::SLUG );
221 - \WP_CLI::log( sprintf( 'minify_html: %s', $opts['minify_html'] ? 'on' : 'off' ) );
314 + // A bare "on" is a lie when the skip guard is active: the
315 + // setting is stored, but Minifier::minify_html() returns its
316 + // input untouched and the delivered HTML is unchanged. Say so
317 + // on the same line, so the contradiction can never be read as
318 + // "minify is broken".
319 + $skip = \XSpeed\Minifier::skip_reason();
320 + $html_state = $opts['minify_html'] ? 'on' : 'off';
321 + if ( $opts['minify_html'] && '' !== $skip ) {
322 + $html_state .= ( 'wp_debug' === $skip )
323 + ? ' (NOT APPLIED — WP_DEBUG is enabled; set WP_DEBUG to false to minify HTML)'
324 + : ' (NOT APPLIED — suppressed by the xspeed_skip_minify filter)';
325 + }
326 + \WP_CLI::log( sprintf( 'minify_html: %s', $html_state ) );
222 327 \WP_CLI::log( sprintf( 'minify_css : %s', $opts['minify_css'] ? 'on' : 'off' ) );
223 328 \WP_CLI::log( sprintf( 'minify_js : %s', $opts['minify_js'] ? 'on' : 'off' ) );
224 329 return;
225 330 }
@@ -230,6 +335,15 @@
230 335 return;
231 336 }
232 337
233 338 \WP_CLI::error( "Unknown action: $action" );
339 + }
340 +
341 + /**
342 + * Minify has no master switch -- it is on when any of minify_html /
343 + * minify_css / minify_js / defer_js / delay_js / async_css /
344 + * remove_query_strings is set. (#363)
345 + */
346 + public function is_active(): ?bool {
347 + return $this->any_bool_flag_on();
234 348 }
235 349 }