๐Ÿณ IntuitiveFE
Login
โ† All concepts

Hydration & Islands Architecture

โฑ๏ธ ~3-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

โ˜• Instant coffee vs fresh brew โ€” SSR HTML is ready immediately (visible) but flat (no interactivity). Hydration adds hot water (JavaScript) to bring it to life.

What hydration actually does

SSR generates HTML on the server. The browser renders it immediately โ€” users see content fast. But static HTML has no event handlers, no React state, no refs. Hydration is the process of React "attaching" to existing DOM nodes: walking the server-rendered HTML, matching it to the virtual DOM, and setting up event listeners without re-rendering.

js
1// Server: render to HTML
2const html = ReactDOMServer.renderToString(<App />);
3// Sends: <button data-reactroot="">Click me</button>
4ย 
5// Client: attach React without re-rendering
6ReactDOM.hydrateRoot(document.getElementById('root'), <App />);
7// React walks the DOM, matches it to <App />'s virtual DOM,
8// attaches onClick handlers, creates state, sets up refs.
9// If server HTML === client HTML: no DOM mutations needed.
10// If mismatch: React logs a warning and re-renders from scratch.

The hydration mismatch problem

Hydration mismatches occur when the HTML the server generated doesn't match the HTML React would generate on the client. React will log a warning and re-render the affected subtree from scratch โ€” breaking the "no re-render needed" optimization.

Date.now()

Fix: Use a static date in SSR, or suppress with suppressHydrationWarning

Math.random()

Fix: Generate random values server-side only, pass as props

window/document access in render

Fix: Guard with typeof window !== 'undefined' or useEffect

Browser-only APIs (localStorage)

Fix: Read in useEffect โ€” never during render

CSS-in-JS without SSR support

Fix: Use SSR-compatible styled-components or use CSS modules

Astro islands โ€” client directives

js
1---
2// Astro component (server-rendered by default)
3const posts = await db.query('SELECT * FROM posts');
4---
5<html>
6 <!-- Static โ€” zero JS, just HTML -->
7 <PostList posts={posts} />
8ย 
9 <!-- Island: hydrate immediately when JS loads -->
10 <SearchBar client:load />
11ย 
12 <!-- Island: hydrate only when component is visible -->
13 <CommentSection client:visible />
14ย 
15 <!-- Island: hydrate when browser is idle -->
16 <Analytics client:idle />
17ย 
18 <!-- Island: hydrate only on media query match -->
19 <MobileMenu client:media="(max-width: 768px)" />
20</html>
๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Hydration strategy

JS size (initial)

800KB

Time to Interactive

2.2s

Traditional React SSR/SSG. Server generates full HTML. Browser downloads the entire JS bundle, parses it, then hydrates the entire component tree before any interaction is possible.

Hydration timeline (from navigation)

0ms
HTML
300ms
JS parse
blocked
1100ms
JS parse
blocked
1400ms
hydrate
blocked
2200ms
interactive
HTML
JS parse
hydrate
interactive
blocked= input ignored
0msFull HTML received from server โ€” page is visible
300ms800KB JS bundle downloading and parsing
1100msJS execution โ€” all module code runs
1400msReact hydrates entire component tree (~1500 components)
2200msPage is fully interactive โ€” all events wired
Gotcha: The page LOOKS ready (HTML rendered) but isn't interactive until hydration completes. Users clicking during the hydration window get no response โ€” or get responses after a noticeable delay.
Insight: There's a gap between FCP (when HTML is visible) and TTI (when React finishes hydrating). For heavy pages with many components, this gap can be 1-3 seconds. The page appears 'frozen' to users during this window.
Explored:๐Ÿ’ง๐Ÿ๏ธ๐Ÿ“ˆ๐Ÿ–๏ธโšก
๐ŸŽฏ

Challenge

Explore all 5 hydration patterns. Track the JS size and TTI for each. Which pattern achieves the fastest TTI? What's the trade-off that makes it possible?

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: What is the "hydration mismatch" error and why does it occur?

React expects the client-rendered HTML to exactly match the server-rendered HTML. If they differ, React logs "Warning: Text content did not match" and falls back to a full client-side re-render โ€” losing the SSR performance benefit. Common causes: using Date.now() or Math.random() in render (different values server vs client), reading browser APIs like localStorage or window.innerWidth during render, or rendering based on cookies that aren't available on the server.

Q: How does Astro's islands differ from React's selective hydration?

React's selective hydration (React 18) is within a single React tree โ€” it prioritizes hydrating components the user is interacting with. All components share state and context. Astro's islands are completely separate React roots โ€” each island is an independent application. Islands cannot share React context, so communication requires non-React mechanisms (custom events, URL params). Astro achieves smaller JS because most of the page doesn't ship any JS at all.

Bug: Rendering different content server vs client

ts
1// โœ— Date.now() produces different values โ†’ mismatch
2function Timestamp() {
3 return <time>{new Date(Date.now()).toLocaleString()}</time>;
4}
5ย 
6// โœ“ Fix 1: Read in useEffect (client-only)
7function Timestamp() {
8 const [time, setTime] = useState('');
9 useEffect(() => { setTime(new Date().toLocaleString()); }, []);
10 return <time suppressHydrationWarning>{time}</time>;
11}
12ย 
13// โœ“ Fix 2: Pass static value as prop from server
14// Server: <Timestamp initial={new Date().toISOString()} />
15function Timestamp({ initial }: { initial: string }) {
16 return <time>{new Date(initial).toLocaleString()}</time>;
17}
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

React 18 selective hydration (hydrateRoot)

js
1// React 18: selective hydration with Suspense
2// Server: streams HTML with Suspense boundaries
3<html>
4 <Header /> {/* streams immediately */}
5 <Suspense fallback={<Spinner />}>
6 <ArticleBody /> {/* streams when ready */}
7 </Suspense>
8 <Suspense fallback={<Spinner />}>
9 <Comments /> {/* independent stream */}
10 </Suspense>
11</html>
12ย 
13// Client: hydrateRoot
14hydrateRoot(document.getElementById('root'), <App />);
15// React hydrates in priority order:
16// 1. Components the user is interacting with (clicking)
17// 2. Above-fold components
18// 3. Below-fold components
19// User can interact with Header while Comments still hydrating

Qwik resumability internals

Qwik serializes component state and event handler function references directly into HTML attributes. The Qwik loader (~1KB) adds a global click listener. On the first interaction, it reads the serialized handler reference from the HTML, lazily downloads the relevant component code, deserializes state, and executes. No component code runs until the user actually interacts with it.

html
1<!-- Qwik serializes handler references into HTML -->
2<button on:click="/src/routes/index_component_div_button_onClick_qhkJn9TsCgI.js#onClick">
3 Add to Cart
4</button>
5<!-- Qwik loader sees this click, downloads the .js chunk,
6 executes onClick โ€” no React hydration ever happened -->

RSC wire format โ€” not HTML, not JSON

React Server Components stream a special wire format (sometimes called the "RSC payload") โ€” not HTML and not JSON. It's a line-delimited format where each line is a serialized React element tree, including client component boundaries and their props. This allows streaming incremental UI updates without full page navigation, and enables the client to build the React component tree without re-running server component logic.

๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

Hydration is React attaching event listeners and internal state to server-rendered HTML without re-creating DOM nodes.

Mismatch occurs when server and client render different HTML โ€” caused by Date.now(), Math.random(), window access, or localStorage reads during render.

Partial hydration hydrates only interactive components; Astro's islands ship zero JS for static components and independently hydrate each interactive island.

React 18 hydrates in priority order โ€” it hydrates the component the user is interacting with first, before completing the rest of the tree.

Qwik serialises component state and event handler references into HTML so the client resumes where the server left off โ€” no re-execution of component code on load.

The page is in the hydration gap โ€” HTML is visible but React hasn't attached event listeners yet. Fix with streaming SSR, selective hydration, or partial hydration.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

How does Next.js App Router handle hydration for Server vs Client Components?