PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.1
1.4.1 1.4.0 1.3.7 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 All 35 releases
← All changes | includes/modules/Minify/MinifyModule.php +186 -26 1.0.5 → 1.4.1 View file →
@@ -18,9 +18,11 @@
18 18 namespace XSpeed\Modules\Minify;
19 19
20 20 defined( 'ABSPATH' ) || exit;
21 21
22 +use XSpeed\Cache;
22 23 use XSpeed\Minifier as LegacyMinifier;
24 +use XSpeed\Minify_Filters;
23 25 use XSpeed\Module;
24 26 use XSpeed\Settings_Manager;
25 27
26 28 final class MinifyModule extends Module {
@@ -30,11 +32,17 @@
30 32 public const VERSION = '1.2.0';
31 33
32 34 public function ui_metadata(): array {
33 35 return array(
34 - 'label' => 'Minify',
35 - 'icon' => 'Wand2',
36 - 'description' => 'Strip whitespace and rewrite enqueued CSS / JS.',
36 + 'label' => __( 'CSS & JavaScript', 'xspeed' ),
37 + 'tab_label' => __( 'Minify', 'xspeed' ), // its own tab on the CSS & JavaScript page
38 + 'icon' => 'Wand2',
39 + 'description' => __( 'Makes HTML, CSS and JavaScript files smaller and loads scripts later.', 'xspeed' ),
40 + 'group' => 'performance',
41 + // Host page: Minify (this module) / Critical CSS (Pro) / Unused
42 + // CSS (Pro) as tabs — the three CSS/JS optimizations live on one
43 + // page instead of three sidebar rows (FBS-83633).
44 + 'custom_panel' => 'CssJsPanel',
37 45 );
38 46 }
39 47
40 48 public function settings_schema(): array {
@@ -41,65 +49,112 @@
41 49 return array(
42 50 'minify_html' => array(
43 51 'type' => 'bool',
44 52 'default' => false,
45 - 'label' => 'Minify HTML',
46 - 'description' => 'Strip whitespace and comments from HTML output. Safe on most themes.',
53 + 'label' => __( 'Minify HTML', 'xspeed' ),
54 + // Names the logged-out caveat up front: minification runs on the
55 + // request that WRITES a cache entry, and should_cache() refuses
56 + // logged-in requests — so "view source while logged in" shows
57 + // un-minified HTML and reads as the feature being broken. (#2)
58 + 'description' => __( 'Removes spaces and comments from your pages. Safe on most themes. Only logged-out visitors see it, so check in a private window.', 'xspeed' ),
47 59 ),
48 60 'minify_css' => array(
49 61 'type' => 'bool',
50 62 'default' => false,
51 - 'label' => 'Minify CSS',
52 - 'description' => 'Compress and rewrite enqueued local stylesheets. External CSS is left untouched.',
63 + 'label' => __( 'Minify CSS', 'xspeed' ),
64 + 'description' => __( 'Makes your site\'s own CSS files smaller. CSS from other domains is left alone.', 'xspeed' ),
53 65 ),
54 66 'minify_js' => array(
55 67 'type' => 'bool',
56 68 'default' => false,
57 - 'label' => 'Minify JavaScript',
58 - 'description' => 'Compress enqueued local scripts. Disable if you hit script-loading conflicts on the frontend.',
69 + 'label' => __( 'Minify JavaScript', 'xspeed' ),
70 + 'description' => __( 'Makes your site\'s own JavaScript files smaller. Turn off if a script on your site stops working.', 'xspeed' ),
59 71 ),
60 72 'defer_js' => array(
61 73 'type' => 'bool',
62 74 'default' => false,
63 - 'label' => 'Defer JavaScript',
64 - 'description' => 'Add defer="defer" to enqueued script tags so they execute after HTML parsing. jQuery + its hard dependencies are skipped automatically.',
75 + 'label' => __( 'Defer JavaScript', 'xspeed' ),
76 + 'description' => __( 'Runs scripts after the page has loaded, so content shows sooner. jQuery and scripts that need it are skipped.', 'xspeed' ),
65 77 ),
66 78 'delay_js' => array(
67 79 'type' => 'bool',
68 80 'default' => false,
69 - 'label' => 'Delay JavaScript Until Interaction',
70 - '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.',
81 + 'label' => __( 'Delay JavaScript', 'xspeed' ),
82 + 'description' => __( 'Scripts load only when the visitor scrolls, taps or types. Pages show much sooner, but test menus and sliders. Cookie consent banners still load first.', 'xspeed' ),
71 83 ),
72 84 'async_css' => array(
73 85 'type' => 'bool',
74 86 'default' => false,
75 - 'label' => 'Load CSS Asynchronously',
76 - '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.',
87 + 'label' => __( 'Load CSS without blocking', 'xspeed' ),
88 + 'description' => __( 'On pages that have critical CSS, the page shows before the rest of its CSS finishes loading. Pages without critical CSS keep loading their CSS normally, because deferring it makes the page show unstyled and then jump.', 'xspeed' ),
77 89 ),
78 90 'remove_query_strings' => array(
79 91 'type' => 'bool',
80 92 'default' => false,
81 - 'label' => 'Remove Asset Query Strings',
82 - 'description' => 'Strip ?ver=X.Y from enqueued CSS / JS URLs. Some CDN caches and proxies cache better when query strings are absent.',
93 + 'label' => __( 'Remove version from file URLs', 'xspeed' ),
94 + 'description' => __( 'Removes ?ver= from CSS and JS links, which some CDNs cache better. After a plugin update, returning visitors may keep old files until their browser cache expires.', 'xspeed' ),
95 + 'advanced' => true,
83 96 ),
84 97 'defer_js_excluded' => array(
85 98 'type' => 'list',
86 99 'default' => array( 'jquery-core', 'jquery-migrate' ),
87 100 'item_type' => 'string',
88 - 'label' => 'Defer / Delay Exclusions',
89 - 'description' => 'Script handles OR URL substrings that skip defer + delay. Defaults exclude jQuery (most themes depend on it being available synchronously). One per line.',
101 + 'label' => __( 'Scripts to never defer or delay', 'xspeed' ),
102 + 'description' => __( 'Script handles or parts of script URLs, one per line. jQuery is listed because most themes need it early; cookie consent banners are skipped on their own.', 'xspeed' ),
103 + // Only relevant once defer OR delay is on — the exclusion list
104 + // governs both. Uses the `any` (OR) dependency form. (FBS-82227)
105 + 'dependsOn' => array(
106 + 'any' => array(
107 + array( 'field' => 'defer_js' ),
108 + array( 'field' => 'delay_js' ),
109 + ),
110 + ),
90 111 ),
112 + 'delay_js_targets' => array(
113 + 'type' => 'list',
114 + 'default' => array(),
115 + 'item_type' => 'string',
116 + 'label' => __( 'Delay only these scripts', 'xspeed' ),
117 + 'description' => __( 'Script handles or parts of script URLs, one per line. Leave empty to delay all scripts; otherwise only these, plus known trackers and chat widgets, are delayed.', 'xspeed' ),
118 + 'info_title' => __( 'Consent banners and Delay JS', 'xspeed' ),
119 + 'info' => sprintf(
120 + /* translators: %s: comma-separated list of consent plugins, each followed by the word to type in parentheses. */
121 + __( '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 Scripts to never defer or delay always wins.', 'xspeed' ),
122 + implode( ', ', Minify_Filters::consent_manager_labels() )
123 + ),
124 + 'dependsOn' => array( 'field' => 'delay_js' ),
125 + ),
126 + 'delay_js_smart' => array(
127 + 'type' => 'bool',
128 + 'default' => false,
129 + 'label' => __( 'Smart delay', 'xspeed' ),
130 + 'description' => __( 'Also delays scripts that code on the page depends on, which helps page builder sites most. The riskiest setting here, so test menus and sliders.', 'xspeed' ),
131 + 'dependsOn' => array( 'field' => 'delay_js' ),
132 + ),
133 + 'delay_js_timeout' => array(
134 + 'type' => 'int',
135 + 'default' => 8000,
136 + 'min' => 0,
137 + 'max' => 60000,
138 + 'label' => __( 'Delay timeout (ms)', 'xspeed' ),
139 + 'unit' => 'ms',
140 + 'description' => __( 'Load delayed scripts after this long if the visitor does nothing. Set 0 to wait for a scroll, tap or key press only.', 'xspeed' ),
141 + 'advanced' => true,
142 + 'dependsOn' => array( 'field' => 'delay_js' ),
143 + ),
91 144 'combine_css' => array(
92 145 'type' => 'bool',
93 146 'default' => false,
94 - 'label' => 'Combine CSS Files',
95 - '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.',
147 + 'label' => __( 'Combine CSS files', 'xspeed' ),
148 + 'description' => __( 'Joins your site\'s own CSS files into one. This helps only on old HTTP/1.1 servers, so leave it off on most hosts.', 'xspeed' ),
149 + 'advanced' => true,
96 150 ),
97 151 'combine_js' => array(
98 152 'type' => 'bool',
99 153 'default' => false,
100 - 'label' => 'Combine JavaScript Files',
101 - '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.',
154 + 'label' => __( 'Combine JavaScript files', 'xspeed' ),
155 + 'description' => __( 'Joins your site\'s own scripts into one file. Turn off if a script stops working after you enable it.', 'xspeed' ),
156 + 'advanced' => true,
102 157 ),
103 158 );
104 159 }
105 160
@@ -159,8 +214,9 @@
159 214 array(
160 215 'name' => 'xspeed minify',
161 216 'callback' => array( $this, 'cli_handler' ),
162 217 'shortdesc' => 'Inspect or purge xSpeed minify cache.',
218 + '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.',
163 219 'synopsis' => array(
164 220 array(
165 221 'type' => 'positional',
166 222 'name' => 'action',
@@ -180,8 +236,26 @@
180 236 * 2. Instantiate the v1 Minifier engine; it now reads from
181 237 * Settings_Manager::get('minify') via its updated read path.
182 238 */
183 239 public function boot(): void {
240 + /*
241 + * Deferred to `init`: both calls below read this module's settings,
242 + * which builds settings_schema(), whose labels go through __().
243 + * boot() runs on `plugins_loaded`, before `after_setup_theme` — the
244 + * earliest point WordPress 6.7+ considers safe to translate — so doing
245 + * it here fires _load_textdomain_just_in_time on every request and
246 + * resolves those labels against an unloaded domain.
247 + *
248 + * Every filter LegacyMinifier registers fires after `init`, so running
249 + * one hook later is equivalent.
250 + */
251 + add_action( 'init', array( $this, 'boot_on_init' ) );
252 + }
253 +
254 + /**
255 + * The real boot body — see boot() for why it runs on `init`.
256 + */
257 + public function boot_on_init(): void {
184 258 $this->seed_from_legacy_if_needed();
185 259 new LegacyMinifier();
186 260 }
187 261
@@ -190,8 +264,45 @@
190 264 // run more than once.
191 265 $this->seed_from_legacy_if_needed();
192 266 }
193 267
268 + /**
269 + * Say so when HTML minification is switched on but suppressed.
270 + *
271 + * `Minifier::skip_reason()` was consulted only by `wp xspeed minify status`
272 + * — the dashboard read "on" while `minify_html()` returned its input
273 + * untouched, so the feature looked broken rather than paused. A field
274 + * report showed a live site with `minify_html: on` and 3,856 indented lines
275 + * delivered, and nothing anywhere explaining the contradiction. (#2)
276 + *
277 + * @return array<int,array<string,mixed>>
278 + */
279 + public function ui_notices(): array {
280 + $opts = $this->get_settings();
281 + if ( empty( $opts['minify_html'] ) ) {
282 + return array();
283 + }
284 +
285 + $reason = LegacyMinifier::skip_reason();
286 + if ( '' === $reason ) {
287 + return array();
288 + }
289 +
290 + // Two different causes, two different fixes — naming the wrong one
291 + // sends the user hunting in the wrong file.
292 + $body = 'wp_debug' === $reason
293 + ? __( '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' )
294 + : __( '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' );
295 +
296 + return array(
297 + array(
298 + 'tone' => 'info',
299 + 'title' => __( 'HTML minification is on but currently paused', 'xspeed' ),
300 + 'body' => $body,
301 + ),
302 + );
303 + }
304 +
194 305 private function seed_from_legacy_if_needed(): void {
195 306 $existing = get_option( 'xspeed_module_minify', null );
196 307 if ( null !== $existing ) {
197 308 return;
@@ -219,9 +330,21 @@
219 330 $action = $args[0] ?? 'status';
220 331
221 332 if ( 'status' === $action ) {
222 333 $opts = Settings_Manager::get( self::SLUG );
223 - \WP_CLI::log( sprintf( 'minify_html: %s', $opts['minify_html'] ? 'on' : 'off' ) );
334 + // A bare "on" is a lie when the skip guard is active: the
335 + // setting is stored, but Minifier::minify_html() returns its
336 + // input untouched and the delivered HTML is unchanged. Say so
337 + // on the same line, so the contradiction can never be read as
338 + // "minify is broken".
339 + $skip = \XSpeed\Minifier::skip_reason();
340 + $html_state = $opts['minify_html'] ? 'on' : 'off';
341 + if ( $opts['minify_html'] && '' !== $skip ) {
342 + $html_state .= ( 'wp_debug' === $skip )
343 + ? ' (NOT APPLIED — WP_DEBUG is enabled; set WP_DEBUG to false to minify HTML)'
344 + : ' (NOT APPLIED — suppressed by the xspeed_skip_minify filter)';
345 + }
346 + \WP_CLI::log( sprintf( 'minify_html: %s', $html_state ) );
224 347 \WP_CLI::log( sprintf( 'minify_css : %s', $opts['minify_css'] ? 'on' : 'off' ) );
225 348 \WP_CLI::log( sprintf( 'minify_js : %s', $opts['minify_js'] ? 'on' : 'off' ) );
226 349 return;
227 350 }
@@ -226,12 +349,49 @@
226 349 return;
227 350 }
228 351
229 352 if ( 'purge' === $action ) {
230 - LegacyMinifier::purge_minified();
231 - \WP_CLI::success( 'Minify cache purged.' );
353 + // Deleting the files alone left every cached page linking names
354 + // that were gone. The assets purge takes the pages with it and
355 + // tells the edge. On a network, a bare `wp xspeed minify purge`
356 + // is a network purge (min/ is shared by every blog); with --url
357 + // it is that site's assets purge.
358 + $network = is_multisite() && ! self::cli_url_given();
359 + $count = $network
360 + ? Cache::purge_assets( 'cli', true )
361 + : Cache::purge_type( 'assets', 'cli' );
362 + \WP_CLI::success(
363 + sprintf(
364 + 'Minify cache purged; %d cached page(s) removed%s.',
365 + $count,
366 + $network ? ' across the network' : ''
367 + )
368 + );
232 369 return;
233 370 }
234 371
235 372 \WP_CLI::error( "Unknown action: $action" );
373 + }
374 +
375 + /**
376 + * Whether this command was pointed at one site with --url.
377 + *
378 + * Only a real WP-CLI run has global config to read. The MCP bridge runs
379 + * the command inside a request for one site, which is the same thing as
380 + * --url, so anything without WP-CLI's config reads as site-scoped.
381 + */
382 + private static function cli_url_given(): bool {
383 + if ( ! class_exists( '\\WP_CLI' ) || ! method_exists( '\\WP_CLI', 'get_config' ) ) {
384 + return true;
385 + }
386 + return '' !== (string) \WP_CLI::get_config( 'url' );
387 + }
388 +
389 + /**
390 + * Minify has no master switch -- it is on when any of minify_html /
391 + * minify_css / minify_js / defer_js / delay_js / async_css /
392 + * remove_query_strings is set. (#363)
393 + */
394 + public function is_active(): ?bool {
395 + return $this->any_bool_flag_on();
236 396 }
237 397 }