Hydration Proof

Search documentation

Find a page or section

Six snapshots of every page, compared in a real browser.

Here is how hydration-proof detects mismatches: it loads each page in a real browser, captures it at six stages, from the bytes the server sent to the settled page, and compares neighboring stages to find where a difference started. A small runtime, injected before any page script, connects to React the way React DevTools does. Your app is not changed.

Connecting to React

hydration-proof drives the browser with Playwright. Before any script of the page runs, it injects a runtime of about 29 KB that stays out of the app's way.

React looks for __REACT_DEVTOOLS_GLOBAL_HOOK__ when it loads, in development and in production builds. The runtime provides that hook, or wraps an existing one so React DevTools and Fast Refresh keep working, and React then tells it about every renderer and every commit.

Inside a commit, React calls the hook before it reports recoverable errors. That lets the runtime capture those errors with their component stack, even when the app (or Next.js) installs its own error handler.

How hydration-proof detects mismatches

The six stages

StageHow it is captured
1. Server HTMLThe body of the document response the browser received, read from the same navigation (never fetched twice)
2. Parsed HTMLThe same bytes loaded again in a page where a Content Security Policy blocks scripts. This is the browser's own parser, so repairs, <noscript> and <template> behave exactly as in the real page
3. Pre-hydration DOMRebuilt by undoing the DOM mutations recorded during React's hydration commit
4. Hydrated DOMSerialized inside the hydration commit (after layout effects), with the props React renders for each element
5. After effectsThe first quiet moment after the hydration commit's effects
6. StableWhen the page stays quiet for ready.quietMs

Every node gets an identity, so the comparison knows whether React reused a node or replaced it. When a page counts as quiet is set by the ready options.

The four comparisons

ComparedFinds
1 → 2Invalid nesting the browser repaired, checked with React's own nesting rules
2 → 3Changes by other scripts and browser extensions before React hydrated (React's streaming moves are undone first)
3 → 4Branches React threw away and rendered again, with the exact text, attribute and element differences inside them
3 ↔ React propsAttributes, styles and text that differ from what React renders, which React 19 does not report or fix in production

Findings from different comparisons are merged, so one bug is reported once, with React's own error attached as evidence. Each finding gets a stable issue code, and which comparison it came from tells you where the difference started: in the markup, in a script that ran before React, or in the React render itself.

Explaining a finding

For every finding, hydration-proof looks up three things:

  • The component and the source line. In development builds React records where each element was created. hydration-proof maps that position through the page's source maps to your file and line, and shows the code. React 18 uses the __source information the compiler adds. For elements created inside libraries (styled-components, UI kits), you get the location of the component that used them. In production builds, the code of the component that rendered the element is found in the loaded scripts and mapped through the browser source maps, when they are available.
  • The likely cause, from the values that differ, the code around that line, the scenario, and the stage where the difference started. The list of causes has a fix guide for each one.
  • What to do about it: advice for the cause, plus the general advice for the issue code.

When a location cannot be proven, the report says so instead of guessing.

Proving a cause with probes

The likely cause is an informed guess. --probe turns it into proof. The page is loaded again with the browser clock and random values fixed, then once for each factor with exactly one thing changed: the clock, the random seed, the locale, the timezone, the theme, the viewport, or browser storage.

If the client value changes, or the finding disappears, when only one factor changed, that factor is the cause. A page that renders differently on an identical reload points at server data. The server is never changed, so fixing the clock in the browser does not hide a time-dependent value.

npx hydration-proof test --probe

Probes lists every factor and what it costs.

Environments and flaky findings

The matrix option tests scenarios in combinations of locales, timezones, themes, viewports, browsers, network and CPU speeds, cache states and custom axes such as feature flags or tenants. Pairwise selection keeps the number of combinations small while every pair of values is still tested at least once. Afterwards, for each route, the report names the environment values that separate the pages with a finding from the pages without it ("Only found with locale de-DE"). See the environment matrix.

--repeat loads every page several times. Findings that come and go are marked flaky, and each page gets a flakiness score.

Streaming, Suspense and navigation

Pages that stream HTML hydrate in steps: the root first, then every Suspense boundary when its content and code arrive. Each of these commits is compared on its own. A mismatch inside one boundary is reported for that boundary, and content React reveals from the stream is not mistaken for a mismatch. A boundary the server could not render, or that React switched to client rendering, is reported with React's error.

The whole document is checked too:

  • <head> values that hydration adds or changes (HP1014). React 19 adds a new <title> or <meta> instead of fixing the server one.
  • duplicate ids (HP3003)
  • useId collisions between several React roots on one page (HP3004)

With checks.navigation, routes are also reached through the app's router, and the result is compared with a direct load; RSC requests are recorded in the timeline. With checks.interactions, the page's scripts are held back while the tool types, clicks and scrolls, so what a user does before the page is interactive is checked as well. Interactions and navigation covers both.

Development and production builds

--mode both starts the app twice: a production build and the development server.

  • Development builds give component names and exact source lines.
  • Production builds show what users get. React 19 in production does not report attribute mismatches at all.

Issues found in only one mode are marked, so you can tell a production-only problem from one you can reproduce locally.

npx hydration-proof test --mode both

What it does not change

Your application code and build are not modified. The runtime exists only in the test browser, and nothing is added to your production bundle.