{"id":"zippvj44g7y4i2n","title":"Hydration and CLS: React Fixes That Survive the Next Deploy","slug":"hydration-cls-react-fixes","summary":"Layout shift after hydration is a logic bug, not a metric curse. I align server HTML, reserve space, and defer DOM surgery.","imageUrl":"https://briancrabtree.me/images/journal-hydration-cls-react-fixes.webp","category":"React","date":"2026-06-05T18:00:00.000Z","featured":false,"likes":14,"author":"Brian Crabtree","content":"<h2>What CLS actually measures</h2>\n\n<p>Cumulative Layout Shift scores unexpected movement during load and early interaction. Fonts, images, ads, and hydration mismatches all move nodes. Google thresholds tightened; 0.1 is not a suggestion for enterprise sites chasing leads.</p>\n\n<p>Lab CLS can differ from field; fix obvious shifts in lab first because they often appear in CrUX later as traffic grows.</p>\n\n<p>ResizeObserver in layoutEffect setting height state causes synchronous layout loops. Defer read/write batches or use CSS grid minmax instead.</p>\n\n<pre><code>// Reserve space before hydration\n&lt;div style={{ minHeight: serverHeight }}&gt;\n  {typeof window === 'undefined' ? &lt;Skeleton /&gt; : &lt;Chart /&gt;}\n&lt;/div&gt;</code></pre>\n\n<p><figure>\n  <img src=\"/images/journal-inline-hydration-cls-fixes.webp\" alt=\"Before and after layout shift during React hydration with reserved space\" width=\"1200\" height=\"675\" loading=\"lazy\" />\n  <figcaption>Reserve space server-side — hydration should not move the layout.</figcaption>\n</figure></p>\n\n<h2>Hydration mismatch sources</h2>\n\n<p>Date formatting in local timezone, random IDs, window width branches, and reading localStorage in render all change client HTML versus server. I move non-determinism to useEffect or pass serialized state from server.</p>\n\n<p>suppressHydrationWarning is for known benign text diffs, not architecture. If warnings flood console, the tree is lying.</p>\n\n<p>Ads that inject after consent still need min-height on slot div. IAB sizes are known; pick the common rectangle reserved space.</p>\n\n<h2>Reserve space before swap</h2>\n\n<p>Skeletons must match final dimensions. A gray box shorter than the loaded card jumps when content arrives. I measure final layout once in design and codify min-height on shells.</p>\n\n<p>Carousels that inject height after measuring children are CLS factories. Set aspect-ratio on slide frames up front.</p>\n\n<p>Portals for modals should not teleport focus before hydration completes on first paint. Delay modal auto-open on landing pages.</p>\n\n<h2>Images and embeds</h2>\n\n<p>width and height on img, aspect-ratio on video wrappers, static poster for players. Lazy ads need reserved slots or collapse gracefully without pushing body text.</p>\n\n<p>React lazy components still need placeholder space at the Suspense boundary. null fallback means zero height until spin, which shifts everything below.</p>\n\n<p>Next/Image handles dimensions well; raw img in MDX content still needs manual width height from editors.</p>\n\n<h2>INP interaction with layout</h2>\n\n<p>Shifts during button tap cause mis-taps on mobile. Fixing CLS often improves INP because users hit intended targets. Do not treat them as separate tickets.</p>\n\n<p>Theme flash and CLS are cousins. Inline script setting data-theme before CSS is same priority as font metrics fix.</p>\n\n<p>When I audit react stacks, INP interaction with layout is where hours disappear if nobody owns the outcome. I write before-and-after notes, attach PSI or waterfall screenshots, and leave the team a three-item follow-up list. Reproducible wins beat toolchain debates in sprint review. If this section matches your bottleneck, send the URL and whether LCP, INP, CLS, or crawl coverage is the pain point.</p>\n\n<h2>Prerender shells</h2>\n\n<p>This site prerenders journal HTML so headlines and hero regions exist before React attaches. Hydration enhances, it does not invent above-fold structure. That pattern cut CLS on marketing routes where Firebase used to load grid after blank frame.</p>\n\n<p>I compare CLS in field versus lab on Search Console when traffic exists. Lab fixes that field ignores mean wrong root cause.</p>\n\n<p>When I audit react stacks, Prerender shells is where hours disappear if nobody owns the outcome. I write before-and-after notes, attach PSI or waterfall screenshots, and leave the team a three-item follow-up list. Reproducible wins beat toolchain debates in sprint review. If this section matches your bottleneck, send the URL and whether LCP, INP, CLS, or crawl coverage is the pain point.</p>\n\n<h2>Debug workflow</h2>\n\n<p>Record Performance panel with layout shifts flagged. Fix the largest sources in order. Ship one change per deploy when stakeholders track metrics. Send a URL with bad CLS; I will point at the shifting node in the trace.</p>\n\n<p>Teaching content editors not to paste 4000px images into CMS is CLS prevention upstream of code.</p>\n\n<p>When I audit react stacks, Debug workflow is where hours disappear if nobody owns the outcome. I write before-and-after notes, attach PSI or waterfall screenshots, and leave the team a three-item follow-up list. Reproducible wins beat toolchain debates in sprint review. If this section matches your bottleneck, send the URL and whether LCP, INP, CLS, or crawl coverage is the pain point. For a related angle I keep coming back to, see <a href=\"/journal/error-boundaries-production-ux/\">Error Boundaries and Production UX Without Infinite Spinners</a>.</p>","tags":["hydration","cls","react"],"views":27}