← back to Dw Theme Boost Fix
TK-10835: structural CLS fix for Boost infinite scroll (reorder __main + main-content compensating spacer) — CLS 0.37->0.03 normal, 0.50->0.027 deep, persists infinite on deep links
c0ff941204a327337c5b8a797fa0339902b54dea · 2026-08-25 10:28:41 -0700 · Steve Abrams
Files touched
M boost-infinite-override.liquid
Diff
commit c0ff941204a327337c5b8a797fa0339902b54dea
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 25 10:28:41 2026 -0700
TK-10835: structural CLS fix for Boost infinite scroll (reorder __main + main-content compensating spacer) — CLS 0.37->0.03 normal, 0.50->0.027 deep, persists infinite on deep links
---
boost-infinite-override.liquid | 123 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 118 insertions(+), 5 deletions(-)
diff --git a/boost-infinite-override.liquid b/boost-infinite-override.liquid
index 70a12f1..190fffe 100644
--- a/boost-infinite-override.liquid
+++ b/boost-infinite-override.liquid
@@ -46,7 +46,7 @@
============================================================================
{% endcomment %}
<script>
-/* DW-GUARD-VERSION 20260825-1000 */
+/* DW-GUARD-VERSION 20260825-1130 */
(function () {
'use strict';
if (window.__dwBoostInfinite) return;
@@ -375,15 +375,128 @@
})();
</script>
{% if request.page_type == 'collection' or request.page_type == 'search' %}
-<style id="dw-anti-collapse">
-/* DW 2026-08-04: reserve each Boost card's image box from inject-time (up the full
- a > div > wrapper chain) so lazy images can't reflow siblings on infinite-scroll
- append. Matches the theme's existing forced 1/1 card look (no visual change). */
+{% comment %}
+ ============================================================================
+ STRUCTURAL CLS FIX (DW, 2026-08-25 — TK-10835 path 2, Steve-approved).
+ ----------------------------------------------------------------------------
+ MEASURED (real Chrome + layout-shift sources API): the Boost product grid
+ lives in `section.product-app--container` (a Shopify section). At
+ DOMContentLoaded it is an ~800px placeholder; when Boost injects the 3-col
+ grid it grows (and grows again on EVERY infinite-scroll append). Because the
+ page furniture below it — the sibling section `__main`, plus the body-level
+ `#dwcw-btn` and `__footer` sections — flows BELOW the grid, each grid growth
+ shoved all of it down. That append-driven shove was the whole jank: CLS
+ decomposed as initial-render ~0.16, scroll-APPEND ~0.21-0.34+ (worse the
+ deeper you scrolled). Total control CLS 0.37 (24 appends) → 0.50 (40).
+
+ Two structural primitives kill it WITHOUT stalling Boost's own bottom
+ detection (Boost fires on `.boost-sd__pagination-infinite-scroll-container-
+ -target`, the sentinel that sits BELOW the grid):
+
+ (A) REORDER — make `.boost-sd__product-filter-fallback` a flex column and
+ push the grid's Shopify section to `order:99` so `__main` renders
+ ABOVE the grid. Grid growth then has no `__main` below it to shove.
+ `:has()` is the fast path; a JS fallback tags the section with
+ `.dw-grid-section` for browsers without `:has()`.
+
+ (B) COMPENSATING SPACER — a 1px-wide, aria-hidden spacer appended as the
+ LAST child of `.main-content`, whose height = (reservedBudget − live
+ content height). As the grid grows, the spacer SHRINKS by the same
+ amount, so `.main-content`'s TOTAL height stays constant → every
+ body-level section after it (`#dwcw-btn`, `__footer`) never re-shoves.
+ Budget is monotonic (bumps only if a huge grid exceeds it → at most one
+ tiny shove, then stable). The Boost sentinel is ABOVE the spacer, so
+ incremental scrolling still reaches it — infinite loading is intact
+ (verified: card count matches control at 336 cards / 40 wheels).
+
+ RESULT (real Chrome, reproducible ×3): normal landing CLS 0.37→0.03,
+ 40-wheel deep stress 0.50→0.027, deep ?page=4 0.155→0.03. Append portion
+ 0.21→0.000. All well under the 0.1 target on BOTH the normal landing and the
+ deep ?page=N case — so infinite scroll now persists on deep links (via the
+ DEEP-LINK NORMALIZE + CASCADE GUARD above) with NO append jank.
+
+ Card-image aspect-ratio reservation (below) is retained for within-batch
+ lazy-image stability; it was never the append-shove culprit (the shifters
+ were __main/__footer/dwcw-btn, not the cards) but it's still correct + free.
+ ============================================================================
+{% endcomment %}
+<style id="dw-cls-structural">
+/* Reserve each Boost card's image box so lazy images can't reflow siblings on
+ append (kept from 2026-08-04; matches the theme's forced 1/1 look). */
.boost-sd__product-item-grid-view-layout-image,
.boost-sd__product-link-image,
.boost-sd__product-link-image > div{ aspect-ratio:1/1 !important; overflow:hidden; display:block; }
.boost-sd__product-image-wrapper,
.boost-sd__product-image{ aspect-ratio:1/1 !important; overflow:hidden; }
.boost-sd__product-image-img{ width:100% !important; height:100% !important; object-fit:cover !important; }
+/* (A) Reorder: grid section renders AFTER __main so grid growth never shoves __main.
+ :has() fast path + .dw-grid-section JS fallback for non-:has() browsers. */
+.boost-sd__product-filter-fallback{ display:flex; flex-direction:column; }
+.boost-sd__product-filter-fallback > .shopify-section:has(section.product-app--container),
+.boost-sd__product-filter-fallback > .shopify-section:has(.boost-sd-container){ order:99; }
+.boost-sd__product-filter-fallback > .shopify-section.dw-grid-section{ order:99; }
</style>
+<script id="dw-cls-structural-js">
+(function () {
+ 'use strict';
+ if (window.__dwClsStructural) return;
+ window.__dwClsStructural = true;
+
+ // (A-fallback) If :has() is unsupported, tag the grid's Shopify section with
+ // .dw-grid-section so the CSS order rule above still fires.
+ function reorderFallback() {
+ try { if (window.CSS && CSS.supports && CSS.supports('selector(:has(*))')) return; } catch (e) {}
+ var fb = document.querySelector('.boost-sd__product-filter-fallback');
+ if (!fb) return;
+ Array.prototype.forEach.call(fb.children, function (ch) {
+ if (ch.querySelector && (ch.querySelector('section.product-app--container') || ch.querySelector('.boost-sd-container'))) {
+ ch.classList.add('dw-grid-section');
+ }
+ });
+ }
+
+ // (B) Compensating shrinking spacer — keeps .main-content total height constant
+ // as the grid grows, so body-level furniture below it (#dwcw-btn, __footer)
+ // never re-shoves on append. Sentinel stays above the spacer → loading intact.
+ var mc, spacer, reserved = 0;
+ function ensure() {
+ mc = mc || document.querySelector('.main-content');
+ if (!mc) return false;
+ if (!document.querySelector('.boost-sd__product-list')) return false;
+ if (!spacer) {
+ spacer = document.createElement('div');
+ spacer.id = 'dw-cls-spacer';
+ spacer.style.cssText = 'width:1px;order:100;flex:0 0 auto;';
+ spacer.setAttribute('aria-hidden', 'true');
+ mc.appendChild(spacer);
+ }
+ return true;
+ }
+ function contentHeight() {
+ var sp = spacer ? (parseFloat(spacer.style.height) || 0) : 0;
+ return Math.ceil(mc.scrollHeight - sp); // .main-content height EXCLUDING our spacer
+ }
+ var stopped = false;
+ function update() {
+ if (stopped || !ensure()) return;
+ // Numbered-pages mode doesn't append → no spacer needed; release if the shopper opted in.
+ try { if (localStorage.getItem('dwPaginationMode') === 'paged') { spacer.style.height = '0px'; stopped = true; return; } } catch (e) {}
+ var c = contentHeight();
+ if (reserved === 0) reserved = c + 3000; // initial budget above current content
+ if (c > reserved - 200) reserved = c + 3000; // budget nearly hit → bump (rare, one tiny shove)
+ spacer.style.height = Math.max(0, reserved - c) + 'px';
+ }
+ function start() {
+ reorderFallback();
+ if (!ensure()) { setTimeout(start, 40); return; }
+ update();
+ try { new ResizeObserver(update).observe(document.querySelector('.boost-sd__product-list')); } catch (e) {}
+ try { new MutationObserver(update).observe(mc, { childList: true, subtree: true }); } catch (e) {}
+ var iv = setInterval(function () { if (stopped) { clearInterval(iv); return; } update(); }, 100);
+ setTimeout(function () { clearInterval(iv); }, 60000);
+ }
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
+ else start();
+})();
+</script>
{% endif %}
← 2b6da47 TK-10835: persist infinite scroll on deep ?page=N landings (
·
back to Dw Theme Boost Fix
·
TK-10835: sync structural CLS fix into rendered snippet (sni 13f27ed →