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/TurboRender/TurboRenderModule.php +118 -0 1.3.7 → 1.4.1 View file →
@@ -75,8 +75,24 @@
75 75 /**
76 76 * Spans whose markup is text, not the page: a section tag inside a
77 77 * JS template string or a comment must not be stamped.
78 78 */
79 + /**
80 + * Background images inside a deferred section stay off until the
81 + * section is near the viewport. Scoped to `.xs-tbg`, which only the
82 + * script below sets: without JavaScript, or without
83 + * IntersectionObserver, nothing is held. Screen only, so a printed page
84 + * keeps every background.
85 + */
86 + private const HOLD_STYLE = '<style id="xspeed-turbo-hold">@media screen{.xs-tbg [data-xspeed-turbo]:not([data-xspeed-near]),.xs-tbg [data-xspeed-turbo]:not([data-xspeed-near]) *{background-image:none!important}}</style>';
87 +
88 + /**
89 + * Releases a section's backgrounds 1000px before it scrolls into view.
90 + * data-xs-nodelay keeps Delay JS from holding it until the first
91 + * interaction, which would leave every deferred background blank.
92 + */
93 + private const HOLD_SCRIPT = '<script id="xspeed-turbo-hold-js" data-xs-nodelay>(function(d){if(!("IntersectionObserver" in window))return;d.documentElement.classList.add("xs-tbg");var io=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){e.target.setAttribute("data-xspeed-near","");io.unobserve(e.target);}});},{rootMargin:"1000px 0px"});function go(){d.querySelectorAll("[data-xspeed-turbo]").forEach(function(s){io.observe(s);});}if(d.readyState!=="loading")go();else d.addEventListener("DOMContentLoaded",go);})(document);</script>';
94 +
79 95 private const MASKED_SPANS = '<script\b[^>]*>.*?</script>|<textarea\b[^>]*>.*?</textarea>|<noscript\b[^>]*>.*?</noscript>|<!--.*?-->';
80 96
81 97 /** Fallback: a child spanning less bytes than this is decoration (an empty notices div, a spacer), not a section. */
82 98 private const FALLBACK_MIN_SPAN = 150;
@@ -262,8 +278,25 @@
262 278 */
263 279 $px = max( 100, (int) apply_filters( 'xspeed_turbo_render_intrinsic_px', self::DEFAULT_INTRINSIC_PX ) );
264 280 $style = '<style id="xspeed-turbo">[data-xspeed-turbo]{content-visibility:auto;contain-intrinsic-size:auto ' . $px . 'px}@media print{[data-xspeed-turbo]{content-visibility:visible}}</style>';
265 281
282 + /**
283 + * Filter whether a deferred section's images and CSS backgrounds wait
284 + * until the visitor scrolls near it.
285 + *
286 + * content-visibility skips a section's rendering, not its downloads:
287 + * the browser still fetched every background a stylesheet gave it,
288 + * and eager images in it. On a live Kadence page a 117 KB row
289 + * background 3,600px down loaded before the hero heading painted,
290 + * and PageSpeed counts every byte that lands before LCP.
291 + *
292 + * @param bool $hold Default true.
293 + */
294 + if ( apply_filters( 'xspeed_turbo_render_hold_media', true ) ) {
295 + $html = self::lazy_section_images( $html );
296 + $style .= self::HOLD_STYLE . self::HOLD_SCRIPT;
297 + }
298 +
266 299 $head_end = stripos( $html, '</head>' );
267 300 return substr_replace( $html, $style, (int) $head_end, 0 );
268 301 }
269 302
@@ -519,8 +552,93 @@
519 552 if ( ! preg_match( '#\G<(/?)[a-zA-Z][a-zA-Z0-9-]*(?:"[^"]*"|\'[^\']*\'|[^>"\'])*>#', $masked, $m, 0, $offset ) ) {
520 553 return null;
521 554 }
522 555 return $offset + strlen( $m[0] );
556 + }
557 +
558 + /**
559 + * Make the images inside stamped sections lazy.
560 + *
561 + * A stamped section is below the fold by Turbo Render's own count, so
562 + * loading="eager" there buys nothing: the Lazy module's eager slot had
563 + * gone to the first image of a slider 5,600px down. Left alone: an
564 + * image marked fetchpriority="high", data-skip-lazy or data-no-lazy,
565 + * one matching Lazy's Excluded Images, and any loading value other
566 + * than eager.
567 + */
568 + private static function lazy_section_images( string $html ): string {
569 + $masked = self::mask( $html );
570 + if ( ! preg_match_all( '#<[a-zA-Z][a-zA-Z0-9-]*\s+data-xspeed-turbo=""#', $masked, $opens, PREG_OFFSET_CAPTURE ) ) {
571 + return $html;
572 + }
573 + $spans = array();
574 + $until = -1;
575 + foreach ( $opens[0] as $open ) {
576 + $start = (int) $open[1];
577 + if ( $start < $until ) {
578 + continue; // Nested inside a span already taken.
579 + }
580 + $end = self::element_end( $masked, $start );
581 + if ( null === $end ) {
582 + continue;
583 + }
584 + $spans[] = array( $start, $end );
585 + $until = $end;
586 + }
587 + if ( empty( $spans ) || ! preg_match_all( '#<img\b(?:"[^"]*"|\'[^\']*\'|[^>"\'])*>#i', $masked, $imgs, PREG_OFFSET_CAPTURE ) ) {
588 + return $html;
589 + }
590 +
591 + $lazy = \XSpeed\Settings_Manager::get( 'lazy' );
592 + $excluded = is_array( $lazy ) && is_array( $lazy['excluded_images'] ?? null ) ? $lazy['excluded_images'] : array();
593 +
594 + $edits = array();
595 + foreach ( $imgs[0] as $img ) {
596 + $at = (int) $img[1];
597 + $in = false;
598 + foreach ( $spans as $span ) {
599 + if ( $at > $span[0] && $at < $span[1] ) {
600 + $in = true;
601 + break;
602 + }
603 + }
604 + if ( ! $in ) {
605 + continue;
606 + }
607 + $tag = substr( $html, $at, strlen( (string) $img[0] ) );
608 + $new = self::lazy_img( $tag, $excluded );
609 + if ( $new !== $tag ) {
610 + $edits[] = array( $at, strlen( $tag ), $new );
611 + }
612 + }
613 + foreach ( array_reverse( $edits ) as $edit ) {
614 + $html = substr_replace( $html, $edit[2], $edit[0], $edit[1] );
615 + }
616 + return $html;
617 + }
618 +
619 + /**
620 + * @param string $tag One <img> tag.
621 + * @param string[] $excluded Lazy's Excluded Images patterns.
622 + */
623 + private static function lazy_img( string $tag, array $excluded ): string {
624 + if ( false !== stripos( $tag, 'data-skip-lazy' ) || false !== stripos( $tag, 'data-no-lazy' )
625 + || \XSpeed\Lazy_Loader::has_high_fetchpriority( $tag ) ) {
626 + return $tag;
627 + }
628 + foreach ( $excluded as $pattern ) {
629 + $pattern = (string) $pattern;
630 + if ( '' !== $pattern && false !== stripos( $tag, $pattern ) ) {
631 + return $tag;
632 + }
633 + }
634 + if ( preg_match( '#(?<![-\w])loading\s*=\s*(["\']?)([^"\'\s>]*)\1#i', $tag, $m, PREG_OFFSET_CAPTURE ) ) {
635 + if ( 'eager' !== strtolower( (string) $m[2][0] ) ) {
636 + return $tag;
637 + }
638 + return substr_replace( $tag, 'loading="lazy"', (int) $m[0][1], strlen( (string) $m[0][0] ) );
639 + }
640 + return (string) preg_replace( '#^<img\b#i', '<img loading="lazy"', $tag, 1 );
523 641 }
524 642
525 643 private static function mask( string $html ): string {
526 644 return (string) preg_replace_callback(