# Client hydration is not blocking above-the-fold content

- **Pattern:** `ab-002067` (`performance-load.rendering.hydration-strategy`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002067
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002067)

## Why it matters

Setting `ssr: false` on above-the-fold components — the header, hero, and navigation — means the server sends empty HTML for those regions, and users see a blank viewport until JavaScript hydrates. This damages First Contentful Paint (FCP) and the perceived speed of the page, even when the full page eventually loads quickly. Browser pre-rendering and crawler indexing also fail for these regions: search engine snapshots taken before hydration miss the content entirely. ISO 25010 performance-efficiency.time-behaviour defines time-behaviour as the time from user action to visible response — a blank viewport is a failure of that response.

## Severity rationale

Low because hydration blocking typically causes a brief flash of blank content rather than a total failure, but on slow connections it extends to several seconds of blank above-the-fold space.

## Remediation

Remove `ssr: false` from above-the-fold components. Server-render the static structure of the header and hero, then hydrate interactive elements after paint. Reserve `ssr: false` for components that genuinely cannot run on the server (browser-only APIs, WebGL, certain chart libraries).

```tsx
// Before — header blank until JS hydrates
const Header = dynamic(() => import('./Header'), { ssr: false })

// After — server renders static header structure immediately
import Header from './Header' // static import, SSR-compatible
// Interactive elements inside Header hydrate after paint

// Pattern: server-render shell, hydrate interactive parts lazily
// app/layout.tsx
export default function Layout({ children }) {
  return (
    <>
      <Header /> {/* server-rendered static markup */}
      <main>{children}</main>
      <DynamicChatWidget /> {/* ssr: false acceptable — below fold, not critical */}
    </>
  )
}

const DynamicChatWidget = dynamic(() => import('./ChatWidget'), {
  ssr: false,
  loading: () => null
})
```

## Detection

- **ID:** `hydration-strategy`
- **Severity:** `low`
- **What to look for:** Count all components rendered above the fold (header, hero, navigation) on the primary landing pages. For each above-the-fold component, check whether it uses `ssr: false`, `dynamic(() => ..., { ssr: false })`, or requires full JavaScript hydration before any content is visible. Classify each as: (a) renders static HTML first, hydrates after paint, or (b) requires JS before any content appears (blank until hydration). Enumerate: "X above-the-fold components found, Y render without requiring JS hydration."
- **Pass criteria:** At least 90% of above-the-fold components render static HTML before hydration. No more than 1 above-the-fold component uses `ssr: false`. Static HTML is served first on all primary landing pages; hydration happens after content is painted. Report: "X of Y above-the-fold components render server HTML before hydration."
- **Fail criteria:** 2 or more above-the-fold components require full JavaScript hydration before any content is visible, or any primary landing page is blank until JavaScript loads.
- **Skip (N/A) when:** Framework does not use hydration (pure server-rendered without client JS, or pure CSR where this check is covered by `no-client-only-seo`).
- **Detail on fail:** `"3 of 5 above-the-fold components use ssr: false — header, hero, and nav are blank until JS loads"` or `"Dashboard layout uses ssr: false for sidebar and chart — 2 above-fold components block rendering"`
- **Remediation:** Use lazy hydration or defer non-critical interactivity:

  ```tsx
  // Before — blocks hydration
  export default function Dashboard() {
    return (
      <>
        <Header /> {/* Heavy interactive component */}
        <Content />
      </>
    )
  }

  // After — static content first, interactivity deferred
  export default function Dashboard() {
    return (
      <>
        <StaticHeader /> {/* Just static markup */}
        <Content />
        <DynamicHeader client:load /> {/* Hydrate dynamically */}
      </>
    )
  }

  // Or lazy hydrate non-critical components
  const LazyInteractiveWidget = dynamic(() => import('./Widget'), {
    loading: () => <Skeleton />,
    ssr: false
  })
  ```

## External references

- iso-25010 performance-efficiency.time-behaviour

Taxons: performance

HTML version: https://auditbuffet.com/patterns/ab-002067
