# How it works

> How hydration-proof detects mismatches: it captures each page at six stages in a real browser, compares them, and explains every finding with a likely cause.

Source: https://hydration.jscrate.dev/docs/how-it-works
Last updated: 2026-09-18

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

| Stage                | How it is captured                                                                                                                                                                                   |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1. Server HTML       | The body of the document response the browser received, read from the same navigation (never fetched twice)                                                                                          |
| 2. Parsed HTML       | The 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 DOM | Rebuilt by undoing the DOM mutations recorded during React's hydration commit                                                                                                                        |
| 4. Hydrated DOM      | Serialized inside the hydration commit (after layout effects), with the props React renders for each element                                                                                         |
| 5. After effects     | The first quiet moment after the hydration commit's effects                                                                                                                                          |
| 6. Stable            | When 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](https://hydration.jscrate.dev/docs/configuration#ready).

### The four comparisons

| Compared        | Finds                                                                                                                |
| --------------- | -------------------------------------------------------------------------------------------------------------------- |
| 1 → 2           | Invalid nesting the browser repaired, checked with React's own nesting rules                                         |
| 2 → 3           | Changes by other scripts and browser extensions before React hydrated (React's streaming moves are undone first)     |
| 3 → 4           | Branches React threw away and rendered again, with the exact text, attribute and element differences inside them     |
| 3 ↔ React props | Attributes, 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](https://hydration.jscrate.dev/docs/issues), 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](https://hydration.jscrate.dev/docs/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.

```bash
npx hydration-proof test --probe
```

[Probes](https://hydration.jscrate.dev/docs/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](https://hydration.jscrate.dev/docs/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](https://hydration.jscrate.dev/docs/issues/hp1014)).
  React 19 adds a new `<title>` or `<meta>` instead of fixing the server one.
- duplicate ids ([HP3003](https://hydration.jscrate.dev/docs/issues/hp3003))
- useId collisions between several React roots on one page
  ([HP3004](https://hydration.jscrate.dev/docs/issues/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](https://hydration.jscrate.dev/docs/interactions)
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.

```bash
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.

## Related

- [Quick start](https://hydration.jscrate.dev/docs/quick-start): install and run your first test
- [Prove the cause of a mismatch with probes](https://hydration.jscrate.dev/docs/probes)
- [Every issue code](https://hydration.jscrate.dev/docs/issues), from HP1001 to HP9010
- [Common causes of hydration errors](https://hydration.jscrate.dev/docs/causes) and their fixes
- [Reports](https://hydration.jscrate.dev/docs/reports): the HTML report, JSON, JUnit and SARIF
