Profiling Hydration with the Chrome DevTools Performance Panel

A PerformanceObserver tells you how long hydration took; the DevTools Performance panel tells you why. When the hydration delta from measuring hydration and interactivity metrics comes back too high, the trace is where you find which island’s mount function is running the long task, whether it is one big block or a cascade of small ones, and whether layout recalculation is inflating the window. This guide is the trace-reading procedure: how to record a representative hydration trace, isolate the hydration long tasks in the flame chart, and read them down to the responsible island component.

Prerequisites


Where Hydration Lives in a Trace

The Performance panel stacks several tracks. Hydration cost is spread across three of them, and reading them together is what turns a wall of coloured bars into an attributable diagnosis.

DevTools Performance Panel — Tracks That Show Hydration Three stacked panel tracks. The Timings track shows two island activation marks. The Interactions track is empty during load. The Main thread track shows a long task whose call tree roots in the framework mount function, flagged with a red long-task corner. Timings island:hydrated:search island:hydrated:cart Interactions (empty during load — populates only when the user interacts) Main Task (long task — 180ms) Evaluate Script hydrateRoot / astro-island <SearchWidget> render ← call tree roots in the mount function: this is hydration

Diagnostic Steps

Step 1 — Configure a representative recording

Goal: Make the trace resemble a real device so the hydration cost you measure actually reproduces in the field.

  1. Open DevTools → Performance.
  2. Click the gear icon and set CPU to 4× slowdown (or 6× for worst-case low-end hardware).
  3. Set Network to a throttled profile (e.g. Slow 4G) so streamed island chunks arrive on a realistic timeline.
  4. Tick Screenshots to correlate paints with hydration, and enable Web Vitals to annotate LCP and INP on the timeline.

Expected output: The capture settings show 4× CPU and a throttled network. Without throttling, hydration long tasks collapse below the 50ms threshold and the trace misleads you into thinking the page is fine.


Step 2 — Record a fresh load

Goal: Capture the trace from navigation so the entire hydration window and every performance.mark is included.

Use the reload-and-record button (the circular arrow), not the plain record button. It restarts navigation and begins capture at the first byte, guaranteeing the hydration marks land inside the trace.

DevTools → Performance → click the ⟳ (reload) capture button
→ wait until the page is visually settled and interactive
→ stop; the trace opens with Timings, Interactions, and Main tracks

Expected output: A trace whose Timings track shows your island:hydrated:* marks and whose Main track shows the load’s script activity. If the Timings track is empty, you used plain record — redo with reload-and-record.


Step 3 — Isolate the hydration long tasks

Goal: Find the tasks that constitute hydration and separate them from application and third-party script.

  1. In the Main track, look for tasks flagged with a red triangle in the top-left corner — these are long tasks (> 50ms) that feed Total Blocking Time.
  2. Open the bottom-up view at the base of the panel and type your framework’s mount function into the filter: hydrateRoot (React), astro-island or hydrate (Astro), qwikloader (Qwik), _hydrate (SolidStart).
  3. Every task whose call tree roots in that function is hydration. Note their combined duration — this is the hydration portion of TBT.

Expected output: A filtered set of long tasks rooted in the mount function, e.g. one 180ms task rooted in hydrateRoot. Script execution rooted elsewhere (analytics, ads) is explicitly excluded, giving you hydration’s true share of blocking.


Step 4 — Attribute cost to a specific island

Goal: Move from “hydration is slow” to “the SearchWidget island is slow.”

  1. Click the long task, then open the Call Tree view.
  2. Expand down from the mount function until you reach your island component frames (visible by name if source maps are served).
  3. Read the Self Time column: the component with the largest self time is the one whose own initialisation — not its children — dominates. That is the island to split or defer.
  4. A deep chain of small self-times instead signals reconciliation width (too many nodes), addressable by extracting static subtrees rather than by deferring.

Expected output: A named island frame — e.g. <SearchWidget> with 96ms self time — identified as the dominant contributor, giving you a concrete target for reducing JavaScript payload in islands.


Verification

Confirm the trace-derived numbers agree with independent instruments before acting.

  1. Marks bound the window. The interval between your first and last island:hydrated:* marks in the Timings track should match the hydration delta reported by PerformanceObserver. A mismatch means a mark is mis-placed (fired before commit) — re-check the lifecycle hook.
  2. Long-task sum equals observed TBT. The combined duration of the hydration long tasks from Step 3 should be within ~10% of the hydration-attributed TBT your observer computed. A large gap usually means Recalculate Style / Layout events are being counted inside the window — subtract them for net JS cost.
  3. Reproducibility. Record three traces and take the median hydration-task duration; warm-CPU variance of ±40ms is normal, so a single trace is never authoritative.
  4. Field correlation. The 4× throttled hydration cost should track the RUM p75 hydration delta. If the trace is far faster than the field, your throttle is too light for your real device mix — step up to 6×.

Troubleshooting

Flame-chart frames show as `(anonymous)` or minified names

Root cause: Production bundles are minified and, without source maps, DevTools cannot resolve frame names back to component identifiers, so hydration frames are unreadable.

Fix: Serve source maps for the profiled build (build.sourcemap in Vite/Astro, productionBrowserSourceMaps in Next.js), or profile a dev build where names are preserved. DevTools will then label the mount call tree with real component names, making Step 4 attribution possible.

No red long-task markers appear even though hydration feels slow

Root cause: Concurrent frameworks split hydration into microtask chunks each under the 50ms long-task threshold, so no single task is flagged even though the cumulative cost is high. The blocking is real but distributed.

Fix: Look for a dense burst of short Evaluate Script tasks in the hydration window rather than one long bar. Their summed duration is the effective blocking. Cross-check with the web-vitals TBT value; a high TBT with no long-task markers confirms the chunked pattern, which is best addressed by reducing total work per measuring hydration and interactivity metrics.

The hydration window includes large `Layout` and `Recalculate Style` blocks

Root cause: An island mounts without a reserved size, forcing a synchronous layout recalculation as it expands. That layout work runs inside the hydration window and inflates the measured cost while also causing layout shift.

Fix: Reserve the island container’s height with min-height or content-visibility: auto plus contain-intrinsic-size so the mount does not trigger reflow. Re-record and confirm the Layout blocks shrink — the remaining time is the island’s true JS hydration cost.


← Back to Measuring Hydration & Interactivity Metrics