Hydration and speed in React and Next.js
A server-rendered React page can look ready and still ignore taps. How hydration works, what it costs, and how Server Components, streaming and splitting help.
In this guide
- What hydration is
- Four ways to build a page
- Why a page can look ready and not respond
- What has changed since the warnings
- Fix 1: send less JavaScript to hydrate
- Fix 2: split what is left, and load it late
- Fix 3: stream the page, and hydrate in pieces
- When you do not need hydration at all
- How to check your own site
- Questions to put to your developer
- Check your own page
- Sources
What hydration is
Many React sites make their HTML on the server, so that the visitor sees content before any script has run. That HTML is only a picture. Nothing in it responds to a tap yet. The React documentation says that hydration attaches your components' logic to the HTML from the server and turns that snapshot into a fully interactive app. The function that does it is hydrateRoot, and a framework such as Next.js usually calls it for you.
Hydration needs the browser to do real work: download the component code, run it, build React's own picture of the page, and attach the event handlers. The React docs add that the result must match the server's HTML, and that mismatches are bugs to fix.
Four ways to build a page
web.dev's article on rendering describes the choices. Each has a cost:
| Approach | HTML is made | What it is good for | What to watch |
|---|---|---|---|
| Static rendering | Ahead of time, once per URL | Fast first paint and a consistently fast first byte. Little script is needed if the page is mostly content. | Every URL needs its own file, which is hard for large or unpredictable sites |
| Server-side rendering | On the server, for each request | Fast first paint, and less script if the server does the work | Building the page takes time, so the first byte can be later |
| Client-side rendering | In the browser, by JavaScript | Apps that behave like software | The script grows with the app, which hurts weak phones and interaction speed |
| Server rendering plus hydration | On the server, then the browser repeats the work | Fast first paint, and a page that works once the scripts run | The gap before it works, and the duplicated work |
Most popular frameworks use the last row by default, as web.dev notes, so it helps to know which one your site uses. One test from web.dev: switch JavaScript off and load the page. A statically rendered page keeps most of its function. A page that needs hydration will mostly sit there.
Why a page can look ready and not respond
Here is the order of events on a server-rendered React page:
- The HTML arrives and the browser paints it. The visitor sees the page. Largest Contentful Paint can be good.
- The scripts download, then the browser parses and runs them.
- React hydrates the components and attaches the handlers.
- Only now does the page respond to everything.
Between steps 1 and 4, the page can ignore a tap or answer it late. web.dev puts it plainly: server-rendered pages can deceptively appear to be loaded and interactive, but cannot respond to input until the scripts have run and the handlers are attached, and it says this can take "seconds or even minutes" on mobile. It calls the state an uncanny valley, where interactivity feels absent although the page appears ready.
The measurements show it as long tasks while the page loads. In a lab test that is Total Blocking Time. In real visits it is a slow Interaction to Next Paint for people who tap early. A page can therefore pass the loading metric and fail the interaction one.
The same work twice
web.dev describes a second cost. To let the browser pick up where the server stopped, without asking for the data again, the server usually puts that data into the page as well. The document then holds the finished HTML, a copy of the data behind it, and the script that builds the same page again. That is more bytes to send and more work to do, for a page that already looks finished.
What has changed since the warnings
The Chrome team's article was written in 2019 and updated in 2023. It encouraged server-side or static rendering over full hydration. It also named two ideas as the way forward: streaming the HTML in chunks, and hydrating the page in pieces instead of all at once.
React has since built both into its design:
- Streaming and selective hydration. The React docs say these are integrated with
<Suspense>. The server sends the page in chunks, and React hydrates the pieces separately. - Server Components. These render ahead of time, on a server or at build time, in an environment apart from the browser. The React docs mark them stable in React 19. Their code does not need to be sent to the browser.
Next.js builds on both. In its App Router, layouts and pages are Server Components by default, and you add interactivity with Client Components.
None of this removes the cost by itself. It gives you ways to reduce it, and the next three sections show them.
Fix 1: send less JavaScript to hydrate
Use Server Components for anything that does not need the browser. The Next.js docs suggest them for fetching data and for reducing the JavaScript sent to the browser. The React docs give an example. A page that loads a Markdown converter and an HTML cleaner in the browser, only to show text that never changes, makes visitors download about 75 KB more (gzipped) and then wait for a second request. Rendered once on the server, those libraries are not in the bundle at all.
Mark only the interactive parts as client code. In Next.js the "use client" directive marks a boundary. Everything the file imports, and every component it renders directly, goes into the client bundle. So the docs say to put the directive on the files that define interactive components instead of marking large parts of the page. Their example is a layout that is mostly a logo and links, with one search box. Only the search box is a Client Component.
Move heavy display work to the server. The Next.js bundling guide points to libraries that only turn data into markup, such as syntax highlighters, chart renderers and Markdown parsers. If the work needs no browser feature or tap, it can run in a Server Component. Then the library does not ship.
Trim packages with many exports. For libraries that export hundreds of items, such as icon sets, Next.js has an optimizePackageImports option (an experimental setting) that loads only the modules you use. Use the bundle analyzer to find the big items first. The general method is in JavaScript bundle size and code splitting.
Fix 2: split what is left, and load it late
Lazy-load client components that are not needed at once. Next.js offers next/dynamic, which is built on React.lazy() and Suspense. A modal that opens on a click, a chart below the fold, or a map can load when needed.
'use client'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('../components/Chart'))The ssr: false option skips the server render for a component. It keeps the code out of the first HTML, so use it only for parts that are not needed on first paint. A component that skips the server render adds nothing to the first view, and can add a layout shift when it arrives. The docs also say ssr: false works only inside Client Components.
Load scripts from other companies late. The Next.js <Script> component has four strategies: beforeInteractive (before any Next.js code and before hydration), afterInteractive (the default, after some hydration), lazyOnload (in the browser's idle time) and worker (experimental, in a web worker). The docs suggest afterInteractive for tag managers and analytics, and lazyOnload for background and low-priority scripts. Keep beforeInteractive for the few that must run first, such as cookie consent. See third-party scripts.
Fix 3: stream the page, and hydrate in pieces
In plain server rendering the server waits for all the data before it sends any HTML. Streaming uses chunked responses to send parts as they become ready. The Next.js docs explain the result: the first byte arrives after the server renders your layouts and the placeholders, and the first paint no longer waits for the slowest data request.
React decides what goes first. The part of the page outside every <Suspense> boundary is called the shell. The shell is sent first, and each boundary's content follows when it is ready. Two details matter:
- Do not wrap the whole app in one boundary. The React docs warn that the shell then holds only a spinner, which can feel slower than waiting a little for the real layout.
- Each boundary is a unit of hydration. According to the Next.js docs, without boundaries React hydrates the entire page in one blocking pass. With them, it works in smaller tasks that yield to the browser, so the page stays responsive. React also gives priority to the part a visitor is interacting with.
Keep the main content out of the streamed parts
Streaming has costs of its own. The Next.js docs list them for the metrics:
- LCP. If the largest element is inside a Suspense boundary, it cannot paint until that boundary's content is swapped in. Keep it outside, or above, the boundaries. Next.js says the same for text and headings.
- LCP images. The
next/imagecomponent can add a preload link in the head, so the image starts to download from the first chunk. In Next.js 16 the oldpriorityprop was replaced bypreload. The docs add that, in most cases,loading="eager"orfetchPriority="high"is the better choice. An image inside a boundary still waits for the swap. - CLS. When a placeholder is replaced by the real content, the page reflows if their sizes differ. Make skeleton placeholders the same size as the content they stand for.
- Buffering. Anything between the server and the visitor that collects the whole response, such as a proxy, a CDN setting, or compression, can remove the benefit. The visitor then gets one late response instead of a progressive one. Test through your real hosting. See what is a CDN.
When you do not need hydration at all
A brochure site, a blog or a documentation page is mostly text. Hydrating it adds cost and little else. web.dev says it is perfectly fine to ship mostly HTML with little JavaScript, and to use different approaches on different pages. Frameworks such as Next.js can make some pages static and others server-rendered. Choose static rendering where the content is the same for everyone, and keep scripts for the parts that really need them.
How to check your own site
- View the source. Use your browser's View Source, not the Elements panel. If your main text and heading are in it, the server made them. If the body is nearly empty, the browser makes everything.
- Do the tap test. On a mid-range phone, load the page and tap a menu button the moment the page looks ready. A delay or no response means hydration finishes late. Repeat with the processor slowed down in DevTools on a computer.
- Record the load in the Performance panel and look at the long tasks after the first paint. If your own bundle fills them, this guide is for you.
- Watch the real-visitor numbers. A good Largest Contentful Paint with a poor Interaction to Next Paint is the classic sign. See how to fix INP.
Questions to put to your developer
- Which pages are static, which are rendered on the server, and which only in the browser?
- How much JavaScript does the home page send, and how much of it runs before the visitor can tap?
- Which components are Client Components, and why?
- Is the largest element on the page in the first HTML, outside any streamed part?
- Which scripts from other companies load, and when?
- Does streaming still work through our CDN and hosting?
Check your own page
Run the free audit. If Largest Contentful Paint is fine and Total Blocking Time is high, look at the Reduce JavaScript execution time and Reduce unused JavaScript findings, and at the longest tasks. A large bundle that runs right after the first paint is the hydration cost.
Sources
- Rendering on the Web on web.dev: the terms, the costs of each approach, and the case against full hydration.
- hydrateRoot, Suspense, Server Components and renderToPipeableStream in the React documentation.
- Server and Client Components, Streaming, Lazy loading, Package bundling, Script and Image in the Next.js documentation.
Start here
Free website audit
Live Google data for any public page: mobile and desktop.
Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it
Rather have this done for you? Speed optimization · Book a call