01 - React Internals & Rendering
1. Hydration
What: The process where React takes server-rendered HTML and attaches event listeners
- makes it interactive on the client. The HTML is already visible — hydration "wakes it up."
How it works:
Server: renderToString(<App />) → static HTML string → sent to browser
Browser: displays HTML immediately (fast First Contentful Paint)
Client JS loads → hydrateRoot(document.getElementById('root'), <App />)
React walks the existing DOM, attaches event handlers, builds its internal treeThe problem: Hydration is ALL or NOTHING. React must process the ENTIRE component tree before anything becomes interactive. On a large page, this can block interactivity for seconds.
Hydration mismatch: If server HTML differs from what the client renders, React throws a warning and re-renders from scratch (destroying the performance benefit). Common causes:
Date.now()orMath.random()in render- Browser-only APIs (
window.innerWidth) used during SSR - Different data on server vs client
// BAD: Hydration mismatch
function Greeting() {
return <p>Current time: {Date.now()}</p>; // different on server vs client
}
// GOOD: Use useEffect for client-only values
function Greeting() {
const [time, setTime] = useState<number | null>(null);
useEffect(() => setTime(Date.now()), []);
return <p>Current time: {time ?? 'Loading...'}</p>;
}2. Partial Hydration
What: Instead of hydrating the entire page, only hydrate the interactive parts. Static content (headers, footers, text) stays as plain HTML — never loaded as JS.
┌────────────────────────────────────┐
│ Header (static HTML - no JS) │
├────────────────────────────────────┤
│ Search Bar (HYDRATED - interactive)│
├────────────────────────────────────┤
│ Article Body (static HTML - no JS) │
├────────────────────────────────────┤
│ Comments (HYDRATED - interactive) │
├────────────────────────────────────┤
│ Footer (static HTML - no JS) │
└────────────────────────────────────┘Key insight: Most of a page is static. Why ship JS for a paragraph of text?
Implementations:
- Astro's
client:*directives (client:load,client:visible,client:idle) - Qwik's resumability (doesn't even hydrate — serializes component state to HTML)
3. Islands Architecture
What: The page is a "sea" of static HTML with "islands" of interactive components. Each island hydrates independently — one island crashing doesn't affect others.
┌──────────────────────────────────────────┐
│ Static HTML (sea) │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Island 1 │ │ Island 2 │ │
│ │ (React) │ │ (Svelte) │ │
│ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────┐ │
│ │ Island 3 │ │
│ │ (Vue) │ │
│ └──────────────────────┘ │
│ │
└──────────────────────────────────────────┘Key difference from partial hydration: Islands can use DIFFERENT frameworks. Each island is isolated, has its own state, and its own JS bundle.
Framework: Astro is the poster child. Fresh (Deno) also uses this.
Trade-off:
- Pro: Ship near-zero JS for static content, each island is independent
- Con: Communication between islands is complex (no shared React tree/context)
- Con: Not great for highly interactive apps where everything is connected
4. Streaming SSR
What: Instead of generating the ENTIRE HTML string then sending it, the server sends HTML in chunks as components finish rendering. The browser renders chunks as they arrive.
Without streaming (traditional SSR):
Server: [render ALL components] → [send complete HTML] → browser paints
User sees: nothing .......... then EVERYTHING at onceWith streaming:
Server: [render header] → send → [render body] → send → [render comments] → send
User sees: header first .... then body .... then commentsReact API:
// React 18+
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/client.js'],
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
pipe(res); // starts streaming
},
});
});Combined with Suspense:
function Page() {
return (
<Layout>
<Header /> {/* sent immediately */}
<Suspense fallback={<Spinner />}>
<SlowDataComponent /> {/* streamed when ready */}
</Suspense>
</Layout>
);
}The server sends the <Spinner /> HTML first, then when SlowDataComponent finishes, it sends a <script> tag that replaces the spinner in the DOM. No full page reload.
5. Concurrent Rendering
What: React can work on multiple state updates at the same time and decide which one to prioritize. It can pause rendering one tree to work on a more urgent update, then come back to it.
Before (synchronous rendering):
User types → React renders entire tree → blocks → user sees result
If tree is large, typing feels laggy because React can't be interrupted.After (concurrent):
User types → React starts rendering → new keystroke arrives →
React ABANDONS current render → starts fresh with new input → fast responseKey mental model: Concurrent rendering makes renders INTERRUPTIBLE and PRIORITIZED.
APIs that enable concurrent rendering:
startTransition()— mark an update as non-urgentuseDeferredValue()— defer a value until urgent work is doneuseTransition()— track pending state of a transition
function Search() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// Input is responsive (query updates immediately)
// List filters on deferredQuery (lower priority, can be interrupted)
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<HeavyFilteredList filter={deferredQuery} />
</>
);
}6. Time Slicing
What: Breaking a large render task into small chunks ("slices") that execute during idle browser frames, yielding back to the browser between slices so it can handle user input, paint, etc.
Without time slicing:
[======== render 50ms ========] → browser blocked, janky
With time slicing:
[== 5ms ==] yield [== 5ms ==] yield [== 5ms ==] yield ...
↑ browser can handle clicks, paint, etc.React's implementation: Fiber architecture enables this. Each fiber node is a unit of work. React's scheduler processes fibers one at a time, checking after each if it should yield to the browser (shouldYield()). If a higher-priority update arrives, it pauses current work.
The 5ms budget: React's scheduler aims to yield every ~5ms, keeping frames at 60fps (16ms per frame).
7. Reconciliation Algorithm
What: The algorithm React uses to determine what changed between the previous render and the current render, so it can apply MINIMAL DOM updates.
Two key heuristics (O(n) instead of O(n³)):
Different type = destroy and rebuild
tsx// Previous: <div><Counter /></div> // Current: <span><Counter /></span> // React: destroys <div> and Counter entirely, rebuilds <span> + new CounterKeys identify elements across renders
tsx// Without key: React doesn't know which item is which // It updates ALL items when one is added/removed // With key: React matches items by key <ul> {items.map(item => <li key={item.id}>{item.name}</li>)} </ul> // Adding an item → React only creates ONE new DOM node
What happens during reconciliation:
- React builds a new "virtual DOM" (fiber tree) from your JSX
- Compares new tree with previous tree (diffing)
- Generates a list of DOM mutations (inserts, updates, deletes)
- Applies mutations to the real DOM in the "commit" phase
8. Fiber Architecture
What: React's internal reimplementation (React 16+) that replaced the old synchronous, recursive "stack reconciler" with an incremental, pausable architecture.
A Fiber is a JavaScript object representing a unit of work:
{
type: 'div', // component type or HTML tag
key: null,
stateNode: domNode, // reference to actual DOM node
child: firstChildFiber, // linked list, not array
sibling: nextFiber, // next sibling
return: parentFiber, // parent
pendingProps: {},
memoizedState: {}, // hooks are stored here as a linked list
effectTag: 'UPDATE', // what DOM operation to perform
alternate: prevFiber, // points to the "current" version (double buffering)
}Double buffering: React maintains TWO fiber trees:
- Current tree: What's currently on screen
- Work-in-progress (WIP) tree: What React is building for the next render
When WIP is complete, React swaps the pointer. The old current becomes the new WIP for the next render.
Why Fiber matters:
- Enables concurrent rendering (each fiber is a unit of work that can be paused)
- Enables priorities (urgent vs deferred updates)
- Enables Suspense (can "suspend" a fiber and show fallback)
- Hooks work because state is stored as a linked list on the fiber node
9. Virtual DOM Diffing Complexity
The theoretical problem: Comparing two arbitrary trees requires O(n³) — for each node, compare with every node in the other tree, then compute optimal transformations.
React's approach: O(n) React cheats with two assumptions:
- Elements of different types produce different trees → don't bother diffing subtrees
- Keys hint at which children are stable across renders
How it diffs children (list diffing):
Old: [A, B, C, D]
New: [A, C, D, B]
Without keys: React updates B→C, C→D, D→B (3 updates)
With keys: React sees B moved to end (1 move operation)Edge case — index as key:
// BAD: Using index as key breaks when order changes
{items.map((item, i) => <Item key={i} data={item} />)}
// If you delete first item, React thinks first item's CONTENT changed,
// second item's content changed, and last item was deleted.
// Every component gets wrong props → bugs + wasted re-renders.10. Suspense Boundaries
What: A component that catches "suspensions" (async operations) in its subtree and shows a fallback until they resolve.
<Suspense fallback={<Skeleton />}>
<UserProfile /> {/* may suspend while fetching data */}
<UserPosts /> {/* may also suspend */}
</Suspense>How it works internally:
- Component "suspends" by throwing a Promise
- React catches it at the nearest Suspense boundary
- Renders the fallback
- When the Promise resolves, React re-renders the suspended component
- Replaces fallback with actual content
Nested boundaries:
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<PostsSkeleton />}>
<Posts /> {/* this suspending only shows PostsSkeleton, not PageSkeleton */}
</Suspense>
</Suspense>With streaming SSR: Server sends fallback HTML first, then swaps in real content via inline <script> when the suspended component finishes rendering on the server.
11. Selective Hydration
What: React 18 feature where the client prioritizes hydrating components that the user is interacting with, rather than hydrating in document order.
Page loads with server HTML:
[Header] [SearchBar] [ArticleBody] [Comments]
Normal hydration: hydrates Header → SearchBar → Article → Comments (in order)
Selective hydration:
User clicks on Comments while React is hydrating SearchBar →
React PAUSES SearchBar hydration →
Hydrates Comments FIRST (because user is interacting with it) →
Then goes back to finish SearchBarRequirements:
- Streaming SSR (
renderToPipeableStream) - Suspense boundaries around sections
- React 18+
Key insight: Hydration becomes event-driven, not sequential.
12. Server Components (RSC)
What: Components that run ONLY on the server. They never ship JS to the client. They can directly access databases, file systems, and APIs.
Server Components Client Components
───────────────── ─────────────────
Run on server only Run on client (and server for SSR)
No JS sent to client JS bundle sent to client
Can access DB directly Cannot access server resources
No state (no useState) Can have state and effects
No event handlers Can handle user events
Can import client comps Cannot import server compsHow RSC works:
- Server renders Server Components into a special serializable format (RSC payload)
- RSC payload is streamed to the client
- Client renders Client Components normally, but uses server-rendered data for Server Components (no JS needed for those)
// Server Component (default in Next.js App Router)
async function ProductPage({ id }: { id: string }) {
const product = await db.products.findById(id); // direct DB access!
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton product={product} /> {/* Client Component */}
</div>
);
}
// Client Component
'use client';
function AddToCartButton({ product }: { product: Product }) {
const [added, setAdded] = useState(false);
return <button onClick={() => setAdded(true)}>Add to Cart</button>;
}When to use which:
- Product pages, dashboards with data-heavy content → Server Components
- Interactive forms, checkout flows → Client Components
- Result: dramatically smaller JS bundles
13. Tearing in Concurrent UI
What: A visual inconsistency where different parts of the UI show different versions of the same data, because a state update happened mid-render.
Imagine: external store has value = 1
Component A reads store → sees 1
← store updates to 2 (mid-render!)
Component B reads store → sees 2
Screen shows: A=1, B=2 → TORN (inconsistent)Why it happens: Concurrent rendering can PAUSE and RESUME. If an external store (Redux, Zustand, module-level variables) changes between pauses, different components read different snapshots.
Solution: useSyncExternalStore
import { useSyncExternalStore } from 'react';
function useStore<T>(store: Store<T>, selector: (state: State) => T): T {
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState()), // getSnapshot
() => selector(store.getInitialState()), // getServerSnapshot
);
}React guarantees consistent reads across the entire render when using this hook. Zustand and Redux Toolkit already use this internally.
14. Scheduler Priorities
What: React's internal scheduler assigns priorities to updates so urgent work (user input) runs before less urgent work (data fetching results).
Priority levels (from React's scheduler):
1. Immediate — synchronous, blocks everything (legacy)
2. UserBlocking — clicks, input (must respond in ~250ms)
3. Normal — data fetching results, setState from useEffect
4. Low — analytics, prefetching
5. Idle — work that can wait indefinitelyHow you influence priority:
// Normal priority (default)
setCount(count + 1);
// Low priority (transition)
startTransition(() => {
setSearchResults(filterHugeList(query));
});
// The setSearchResults update is interruptible and won't block input15. Render Waterfalls
What: When data fetching is sequential because each component fetches after its parent renders, creating a chain of loading states.
BAD (waterfall):
ParentPage renders → starts fetching user data
... waiting ...
ParentPage re-renders with user → ChildProfile renders → starts fetching posts
... waiting ...
ChildProfile re-renders with posts → GrandchildComments renders → starts fetching comments
Timeline: ===fetch user=== ===fetch posts=== ===fetch comments===
↑ everything visibleGOOD (parallel):
ParentPage renders → starts ALL fetches in parallel
===fetch user===
===fetch posts===
===fetch comments===
↑ everything visibleSolutions:
- Fetch in parent, pass down: Parent fetches all data, children receive via props
- Parallel queries: TanStack Query's
useQueriesor multipleuseQueryin same component - Route-level fetching: Next.js
loader/ Remixloader— fetch before rendering - Server Components: Fetch on server, no client waterfall at all
16. Edge Rendering
What: Running server-side rendering at CDN edge locations (close to the user) instead of a central origin server.
Traditional SSR:
User (Mumbai) → CDN → Origin Server (US) → render → HTML → CDN → User
Latency: ~200ms
Edge Rendering:
User (Mumbai) → Edge (Mumbai) → render → HTML → User
Latency: ~20msPlatforms: Cloudflare Workers, Vercel Edge Functions, Deno Deploy
Constraints:
- Limited runtime (V8 isolates, not full Node.js)
- No file system, limited memory
- Cold starts must be <5ms
- Can't use all npm packages (no native modules)
When to use: Personalized content (user-specific dashboards), A/B testing, geo-specific content. Static content is better served from CDN cache directly.