Next.js. Sanity CMS sections. Shopify homepage. Vercel Speed Insights watched CLS. If a section streamed in from height 0, the product grid punched the thumb. P75 can stay green while the tail eats the page.
A homepage that stamps itself into place. Grid, fold, leftover strips. The median visitor on a warm cache never sees it. The P95 visitor does. They call it jank. Vercel Speed Insights calls it Poor CLS.
I wrapped every Sanity CMS section on a production Shopify storefront in a 'use client' error boundary. Inside it sat an empty React <Suspense>. That boundary is load-bearing. Next.js recovers an async server throw only at a Suspense edge. Without one, the whole shell 500s. I wanted isolated recovery. I got that. I also got height 0.
The Shopify homepage is a stack of async Sanity server sections. While each one resolved, the fallback rendered nothing. Zero height. Then the HTML arrived and the block appeared. One section is a twitch. A page of Sanity sections is a stampede.
Vercel Speed Insights told two stories. P75 CLS stayed 0. P95 CLS went Poor. P75 is the wrong success signal. The median visitor never saw the stream. Cached HTML, warm RSC payload, fast region. The tail did: cold cache, slow CMS, first paint of collapsed slots. Those people watched the product grid, the fold, and the leftover strips pop in under their thumb. That is a layout shift, not a recovery.
An empty fallback is honest about having nothing to show. It is a lie about layout. I did not need another Lighthouse lecture. I needed reserved height.
The fix is type-keyed, not min-h-svh on every section. Oversized fallbacks that collapse when real content is shorter are also CLS. You trade a jump down for a jump up. Same metric, prettier skeleton.
So the helper keys on section type. The collection grid, sectionSearchFiltersGrid, gets min-h-[80vh]. Large fold sections get min-h-80. Everything else gets min-h-32. Only the product-grid section is critical: it rethrows to the route boundary instead of returning null. A Shopify storefront without a grid is not a storefront. A Shopify storefront without a promo strip still is.
Empty fallbacks collapse the Shopify storefront, then shove every Sanity CMS section down the page. Reserved height holds the slots. Demo boxes are scaled. Production uses the real classes on the labels.
Fallbacks are height 0. Stream to watch the Shopify storefront reconstruct under the fold.
export function sectionSuspenseFallbackClassName(sectionType: string): string {
if (sectionType === 'sectionSearchFiltersGrid') return 'w-full min-h-[80vh]';
if (LARGE_FOLD_SECTION_TYPES.has(sectionType)) return 'w-full min-h-80';
return 'w-full min-h-32';
}
// inside the client error boundary
return (
<Suspense fallback={<div aria-hidden className={sectionSuspenseFallbackClassName(sectionType)} />}>
{children}
</Suspense>
);Toggle the demo. Height 0 looks clean until content lands. Reserved height looks slightly empty, then stays put. I still keep the Suspense. React still needs that boundary. I just stop pretending zero pixels is a layout.
Happy coding! Sander