Sander Korf
  • Portfolio
  • Resume

Sander Korf

Dutch Full Stack AI Engineer helping businesses cut costs through intelligent automation. 13+ years turning expensive manual processes into systems that work 24/7. Based in Amsterdam, available for freelance projects.

Navigation

  • Portfolio
  • Resume

Legal

  • Cookies
  • Privacy Policy
  • Terms and Conditions

Blog

Algolia

  • Algolia InstantSearch INP and object identity
  • Algolia merch by Sanity _type, not title
  • Algolia InstantSearch search key, not admin

Vercel

  • Next.js empty Suspense fallbacks wreck CLS P95
  • Vercel Workflows: skip closed eToro legs

eToro

  • eToro deploy vs rotate: idle cash not rotate
  • eToro search 404: query fields, not ticker
  • eToro v2 opens at-most-once, settlement
  • eToro LLM overlay ±10pp: critic cannot veto

Next.js

  • next-intl useTranslate skip links above fold
  • next-intl owns Klaviyo nl-NL email copy

Firebase & Expo

  • Staging deep links must not hijack production
  • Turn off Worklets Bundle Mode for EAS SHA-1
  • Reclaim Firebase orphan without password wipe

Centra & Klaviyo

  • Centra to Klaviyo tags need a plugin contract

About

  • Full Stack AI Engineer in Amsterdam

© 2026 Sander Korf. All rights reserved

94719489

  • Applied AI
  1. Home
  2. Blog
  3. Next.js empty Suspense fallbacks wreck CLS P95

Next.js empty Suspense fallbacks wreck CLS P95

Empty Suspense fallbacks made my CMS blocks jump in late and wrecked layout stability. I reserve min-height per section type and watch P95, not P75.

Sander KorfPublished August 31, 20263 min read
nextjssanityshopifyvercelcls

This was a Shopify storefront streaming Sanity

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.

What they would notice if it failed

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.

P75 said we were fine

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.

Empty is still a shift

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.

Height 0 vs type-keyed min-height

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.

Footer / next section. Watch this jump on empty. It holds on reserved height.

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