on interaction).
add_filter( 'script_loader_tag', array( Minify_Filters::class, 'delay_script_tag' ), 30, 3 );
add_action( 'wp_footer', array( Minify_Filters::class, 'print_delay_bootstrap' ), 1000 );
// script_loader_tag only fires for wp_enqueue_script()'d assets.
// Analytics / pixel / chat-widget tags printed straight into
// wp_head bypass it, and those are usually the heaviest scripts
// on the page — so sweep the finished buffer too. Runs before
// minify_html (same filter, default priority) and is baked into
// the cache file, so it replays on static hits where PHP never
// boots.
add_filter( 'xspeed_cache_final_html', array( Minify_Filters::class, 'delay_raw_script_tags' ), 20 );
// The vendors' own install snippets are INLINE (no src at all),
// so the src sweep above never sees them; this one parks an
// inline body that names a known third-party host.
add_filter( 'xspeed_cache_final_html', array( Minify_Filters::class, 'delay_inline_snippets' ), 21 );
}
if ( ! empty( $opts['async_css'] ) ) {
add_filter( 'style_loader_tag', array( Minify_Filters::class, 'async_style_tag' ), 20, 2 );
// style_loader_tag only fires for wp_enqueue_style()'d sheets.
// Themes print Google/Bunny/Typekit font CSS as literal
// markup in the head, so those sheets never reach the filter and
// stay render-blocking — sweep the finished buffer for the known
// font-CSS hosts. Baked into the cache file, so it replays on
// static hits where PHP never boots.
add_filter( 'xspeed_cache_final_html', array( Minify_Filters::class, 'async_raw_font_css_links' ), 22 );
}
/*
* CSS combining runs on the FINISHED HTML, not the enqueue queue.
*
* The queue-walking version could not be made correct: whatever it
* wrote at priority 999, WordPress edited afterwards. Core's
* wp_maybe_inline_styles() inlines any queued handle carrying a `path`
* and sets src=false on it, which silently threw away the combined URL
* and took the sheets we had blanked with it — six stylesheets became
* one and the site rendered unstyled. See Css_Combine_Buffer's header
* for the full trace. (#195)
*
* Two entry points, because the page cache's filter is not always
* available: `xspeed_cache_final_html` fires only on a cacheable MISS,
* so on a site with the cache off — or on an excluded URL like /cart —
* combining would silently stop working. Css_Combine_Buffer::boot()
* opens its own buffer in exactly those cases and no-ops otherwise, so
* the page is transformed once either way.
*/
if ( ! empty( $opts['combine_css'] ) ) {
add_filter( 'xspeed_cache_final_html', array( Css_Combine_Buffer::class, 'process' ), 5 );
Css_Combine_Buffer::boot();
}
if ( ! empty( $opts['combine_js'] ) ) {
// JS stays on the enqueue path for now: dependency order,
// async/defer and wp_add_inline_script make it a different
// problem, and the reported break is CSS-only. Moving it is worth
// its own change rather than doubling the blast radius here.
add_action( 'wp_enqueue_scripts', array( Asset_Combiner::class, 'combine_scripts' ), 999 );
}
}
/**
* HTML elements that participate in an inline formatting context, where
* whitespace between two of them renders as a visible space.
*
* Deliberately excludes
(nothing to separate) and replaced/embedded
* inline elements that sit alone. Anything not listed is treated as block
* level, where inter-tag whitespace collapses to nothing and is safe to
* strip. (FBS-84090)
*/
private const INLINE_TAGS = array(
'a', 'abbr', 'b', 'bdi', 'bdo', 'cite', 'code', 'data', 'del', 'dfn',
'em', 'i', 'ins', 'kbd', 'label', 'mark', 'q', 'rp', 'rt', 'ruby',
's', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u',
'var', 'wbr', 'img', 'button', 'select', 'output',
);
/** True when $tag renders inline, so whitespace beside it is visible. */
private static function is_inline( string $tag ): bool {
return in_array( strtolower( $tag ), self::INLINE_TAGS, true );
}
/**
* Why minification is being skipped, when it is. '' when it will run.
*
* minify_html can read "on" in every settings surface while producing
* byte-identical HTML, because the guard below silently returns the
* input. Field report: a live site showed `minify_html: on` with 3,856
* indented lines in the delivered HTML and nothing anywhere explaining
* the contradiction — the setting looked broken rather than suppressed.
* Callers that report status MUST consult this so the refusal is
* visible. (Same class as Cache::static_rewrite_block_reason().)
*
* @return string 'wp_debug', 'filter', or ''.
*/
public static function skip_reason(): string {
$debug_skip = defined( 'WP_DEBUG' ) && WP_DEBUG;
if ( ! apply_filters( 'xspeed_skip_minify', $debug_skip ) ) {
return '';
}
// Distinguish the built-in WP_DEBUG rule from a third party
// filtering the escape hatch — the fixes are different.
return $debug_skip ? 'wp_debug' : 'filter';
}
public static function minify_html( $html ) {
if ( '' !== self::skip_reason() ) {
return $html;
}
$placeholders = array();
$pattern = '#<(pre|textarea|script|style)\b[^>]*>.*?\1>#is';
$html = preg_replace_callback(
$pattern,
function ( $m ) use ( &$placeholders ) {
$key = '__XSPEED_PH_' . count( $placeholders ) . '__';
// The placeholder pass exists to protect content whose
// whitespace is significant (
,