Hydration is the price of interactivity on top of a server-rendered page. On a fast laptop the price is invisible. On a mid-range Android phone over 4G it is the single biggest source of perceived slowness — the moment the page looks ready, you tap a button, and nothing happens for half a second while the framework finishes attaching its handlers.
What hydration actually does
The server renders the page to HTML and sends it. The browser paints it — that is your FCP, often well under a second. Then the JavaScript bundle finishes downloading, the framework runtime parses, and the framework walks the existing DOM, attaches event handlers, and reconciles the static markup with its component tree. Until that walk finishes, the page looks interactive but is not. Every click before then is queued and processed in a single burst the moment hydration completes.
The cost has three components: the bytes of JavaScript, the parse and compile time on the device, and the main-thread work of the hydration walk itself. The first two scale with bundle size; the third scales with the number of components and the depth of the tree.
Why the cost grew
Frameworks have been adding capability for ten years: hooks, context, suspense, transitions, server actions, view transitions. Each is useful and each ships a few more kilobytes of runtime. The trade is real: developers got nicer ergonomics and users got more JavaScript. Hydration cost is the visible bill for that trade.
The three architectural responses below all attempt to send less JavaScript to the client by changing the boundary between server and client work. They differ in where that boundary lives and how much the developer has to think about it.
1. Islands architecture
The page is mostly static HTML. Specific components — the search box, the comment widget, the embedded calculator — are marked as interactive islands. Only those islands ship JavaScript. The frameworks here are Astro, Fresh, and (to a degree) Eleventy.
---
// An Astro page — the rest of this file is static HTML
import SearchBox from '../components/SearchBox.jsx';
---
<article>
<h1>{title}</h1>
<SearchBox client:load />
<div set:html={renderedMarkdown} />
</article>The directive client:load tells Astro to hydrate this component on page load. Without it, the component renders to HTML and ships zero JavaScript. Other directives — client:idle, client:visible, client:media — let you defer the hydration of an island until it is needed.
The model fits a content site well: a blog, a documentation site, a marketing page with a few interactive widgets. It fits a dashboard or an app surface poorly, because nearly every component would be an island and you would be opting into hydration everywhere.
2. Server components
Components run on the server and serialise their output as part of the response. They never hydrate on the client because they never existed on the client. The runtime is Next.js App Router and the same pattern is available in Remix and TanStack Start.
// app/dashboard/page.tsx — server component by default
import { getMetrics } from '@/lib/data';
import LiveChart from './LiveChart'; // client component
export default async function DashboardPage() {
const metrics = await getMetrics();
return (
<main>
<h1>Dashboard</h1>
<MetricSummary metrics={metrics} />
<LiveChart initialData={metrics.timeseries} />
</main>
);
}DashboardPage and MetricSummary run on the server. They do not ship JavaScript. LiveChart is marked "use client" at the top of its file because it needs state and effects; that one component, plus its dependencies, is what ships.
The model fits a B2B SaaS dashboard well — most of the page is data display, which is exactly what server components are good at, and the interactive parts are localised. It demands more architectural thinking: you have to pick the client boundary carefully, and you cannot import a client-only library into a server component.
3. Selective and progressive hydration
The page still hydrates fully, but the framework chooses the order intelligently. React 18's selective hydration prioritises the component the user interacted with first, so a tap on the header search box stops the hydration walk that was busy attaching the footer and gets the search box live first.
Qwik's resumability goes further. The framework serialises every piece of application state — event handlers, component state, the framework itself — into the HTML, so the client does not need to re-execute any of it. When the user clicks, Qwik lazy-loads only the specific handler that the click needs. An app of any size starts interactive in roughly the same time.
The trade is mental-model complexity (Qwik) versus a more familiar model with smaller wins (React 18). Most production apps in 2026 are still on React without Qwik, so the selective-hydration win is the one most teams will actually feel.
Which pattern fits which app shape
- Content site (blog, docs, marketing, e-commerce catalogue): islands. The page is mostly static; ship JavaScript only where you need it.
- B2B SaaS dashboard: server components. Heavy data display, localised interactivity, and a clean boundary between "render this data" and "handle this user action".
- Two-thirds static / one-third interactive (product page with a configurator, news site with a comment thread): server components with carefully placed client-component leaves. The configurator is one client component; everything around it stays on the server.
- Pure SPA app surface (a Figma-style canvas, a spreadsheet): hydration is unavoidable because almost everything is interactive. The wins here come from bundle size and selective hydration, not from server components.
Measuring hydration cost
The gap between FCP and TTI (Time to Interactive) is a fair proxy for hydration cost. A more precise measurement uses PerformanceObserver to watch for long tasks during the post-FCP window:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.log('long task', entry.duration, entry.startTime);
}
}
});
observer.observe({ entryTypes: ['longtask'] });Run this on a mid-range Android over throttled 4G. The cluster of long tasks between FCP and the moment buttons start responding is the hydration walk. If that cluster sums to more than 200 ms, INP in the field will reflect it.
The single biggest win in App Router
The most common architectural mistake in a Next.js App Router codebase is the page that begins with "use client" at the top of the file. That directive marks the file and everything it imports as a client component. A page that wraps its whole layout in "use client" ships the entire route as client JavaScript, defeating the point of the App Router.
The fix is small but disciplined: keep page.tsx as a server component, push "use client" down to the smallest leaves that actually need it, and pass server-fetched data into those leaves as props. The leaves can have state, can use hooks, can use browser APIs; the rest of the page does not ship.
The habit that compounds
The right hydration strategy is the one you can measure. Pick the architecture that fits the shape of your app, draw the client boundary deliberately, and watch INP in the field every week. Frameworks will keep adding capability; the bill in JavaScript will keep growing. The teams that ship genuinely fast products are the ones who treat that boundary as a first-class part of the design, not as something the framework defaults handle for them.
Related reading
For the metric that surfaces hydration cost in production, see the Core Web Vitals working definition. The complementary lever — sending less JavaScript in the first place — is covered in the bundle size audit guide. For the concrete App Router patterns that draw a clean server/client boundary, see server vs client components in practice. All three sit in the web performance topic.
About the writers
Founder of ShareCode. Writes the engineering deep-dives on this site — WebRTC, Firebase Auth, real-time sync, and the production patterns behind the editor itself.
More from Kishan
Developer educator at ShareCode. Writes the tutorial track — Python, JavaScript debugging, coding-interview prep, and the everyday code-quality habits that hold up in real codebases.
More from Kajal
Picking a hydration strategy for a new app?
Sketch the page shape — what is static, what is interactive — into a code space, share the link with a colleague, and decide together whether the route belongs in an islands page, a server-component shell, or a client-rendered surface. The decision is much easier to make on a drawing than on a half-built codebase.
Open a code space →