12 min read
Part of Series: React Development & Architecture
  1. React Hooks
  2. React Architecture Internals

React Architecture Internals

This comprehensive analysis examines React’s sophisticated architectural evolution from a simple Virtual DOM abstraction to a multi-faceted rendering system that spans client-side, server-side, and hybrid execution models. We explore the foundational Fiber reconciliation engine, the intricacies of hydration and streaming, and the revolutionary React Server Components protocol that fundamentally reshapes the client-server boundary in modern web applications.

React’s original reconciliation algorithm operated on a synchronous, recursive model that was inextricably bound to the JavaScript call stack. When state updates triggered re-renders, React would recursively traverse the component tree, calling render methods and building a new element tree in a single, uninterruptible pass. This approach, while conceptually straightforward, created significant performance bottlenecks in complex applications where large component trees could block the main thread for extended periods.

React Fiber, introduced in React 16, represents a complete architectural reimplementation of the reconciliation process. The core innovation lies in replacing the native call stack with a controllable, in-memory data structure—a tree of “fiber” nodes linked together in a parent-child-sibling relationship. This virtual stack enables React’s scheduler to pause rendering work at any point, yield control to higher-priority tasks, and resume processing later.

Each fiber node serves as a “virtual stack frame” containing comprehensive metadata about a component and its rendering state:

// Simplified fiber node structure
const fiberNode = {
// Component identification
tag: "FunctionComponent", // Component type classification
type: ComponentFunction, // Reference to component function/class
key: "unique-key", // Stable identity for efficient diffing
// Tree structure pointers
child: childFiber, // First child fiber
sibling: siblingFiber, // Next sibling at same tree level
return: parentFiber, // Parent fiber (return pointer)
// Props and state management
pendingProps: newProps, // Incoming props for this render
memoizedProps: oldProps, // Props from previous render
memoizedState: state, // Component's current state
// Work coordination
alternate: workInProgressFiber, // Double buffering pointer
effectTag: "Update", // Type of side effect needed
nextEffect: nextEffectFiber, // Linked list of effects
// Scheduling metadata
expirationTime: timestamp, // When this work expires
childExpirationTime: timestamp, // Earliest child expiration
}

The alternate pointer is central to Fiber’s double-buffering strategy. React maintains two fiber trees simultaneously: the current tree representing the UI currently displayed, and the work-in-progress tree being constructed in the background. The alternate pointer links corresponding nodes between these trees, enabling React to build complete UI updates without mutating the live interface.

Fiber’s reconciliation process operates in two distinct phases, a design choice that directly enables concurrent rendering capabilities:

The render phase determines what changes need to be applied to the UI. This phase is asynchronous and interruptible, making it safe to pause without visible UI inconsistencies:

  1. Work Loop Initiation: React begins from the root fiber, traversing down the tree
  2. Unit of Work Processing: Each fiber is processed by performUnitOfWork, which calls beginWork() to diff the component against its previous state
  3. Progressive Tree Construction: New fibers are created and linked, gradually building the work-in-progress tree
  4. Time-Slicing Integration: Work can be paused when exceeding time budgets (typically 5ms), yielding control to the browser for high-priority tasks
// Simplified work loop structure
function workLoop(deadline) {
while (nextUnitOfWork && deadline.timeRemaining() > 1) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
if (nextUnitOfWork) {
// More work remaining, schedule continuation
requestIdleCallback(workLoop)
} else {
// Work complete, commit changes
commitRoot()
}
}

Once the render phase completes, React enters the synchronous, non-interruptible commit phase:

  1. Atomic Tree Swap: The work-in-progress tree becomes the current tree via pointer manipulation
  2. DOM Mutations: React applies accumulated changes from the effects list
  3. Lifecycle Execution: Component lifecycle methods and effect hooks are invoked in the correct order

This two-phase architecture is the foundational mechanism that enables React’s concurrent features, including Suspense, time-slicing, and React Server Components streaming.

React implements an O(n) heuristic diffing algorithm based on two pragmatic assumptions that hold for the vast majority of UI patterns:

  1. Different Element Types Produce Different Trees: When comparing elements at the same position, different types (e.g., <div> vs <span>) cause React to tear down the entire subtree and rebuild from scratch, rather than attempting to diff their children.

  2. Stable Keys Enable Efficient List Operations: When rendering lists, the key prop provides stable identity for elements, allowing React to track insertions, deletions, and reordering efficiently. Without keys, React performs positional comparison, leading to performance degradation and potential state loss.

React Hooks are deeply integrated with the Fiber architecture. Each function component’s fiber node maintains a linked list of hook objects, with a cursor tracking the current hook position during render:

// Hook object structure
const hookObject = {
memoizedState: currentValue, // Current hook state
baseState: baseValue, // Base state for updates
queue: updateQueue, // Pending updates queue
baseQueue: baseUpdateQueue, // Base update queue
next: nextHook, // Next hook in linked list
}

The Rules of Hooks exist precisely because of this index-based implementation. Hooks must be called in the same order on every render to maintain correct alignment with the fiber’s hook list. Conditional hook calls would desynchronize the hook index, causing React to access incorrect state data.

In CSR applications, the browser receives a minimal HTML shell and JavaScript constructs the entire DOM dynamically:

// CSR initialization
import { createRoot } from "react-dom/client"
const root = createRoot(document.getElementById("root"))
root.render(<App />)

Internally, createRoot performs several critical operations:

  1. FiberRootNode Creation: Establishes the top-level container for React’s internal state
  2. HostRoot Fiber Creation: Creates the root fiber corresponding to the DOM container
  3. Bidirectional Linking: Links the FiberRootNode and HostRoot fiber, establishing the fiber tree foundation

When root.render(<App />) executes, it schedules an update on the HostRoot fiber, triggering the two-phase reconciliation process.

CSR Trade-offs: While CSR provides fast Time to First Byte (TTFB) due to minimal initial HTML, it results in slow First Contentful Paint (FCP) and Time to Interactive (TTI), as users see blank screens until JavaScript execution completes.

SSR addresses CSR’s blank-screen problem by pre-rendering HTML on the server, but introduces the complexity of hydration—the process of “awakening” static HTML with interactive React functionality.

Hydration is not a full re-render but rather a reconciliation between server-generated HTML and client-side React expectations:

// React 18 hydration API
import { hydrateRoot } from "react-dom/client"
hydrateRoot(document.getElementById("root"), <App />)

The hydration process involves:

  1. DOM Tree Traversal: React traverses existing HTML nodes alongside its virtual component tree
  2. Event Listener Attachment: Interactive handlers are attached to existing DOM elements
  3. State Initialization: Component state and effects are initialized without re-creating DOM nodes
  4. Consistency Validation: React validates that server and client rendering produce identical markup

Hydration Mismatches occur when server-rendered HTML doesn’t match client expectations. Common causes include:

  • Date/time rendering differences between server and client
  • Conditional rendering based on browser-only APIs
  • Random number generation or unstable keys

Progressive Hydration addresses traditional hydration’s all-or-nothing nature:

// Progressive hydration with Suspense
import { lazy, Suspense } from "react"
const HeavyComponent = lazy(() => import("./HeavyComponent"))
function App() {
return (
<div>
<CriticalComponent />
<Suspense fallback={<Skeleton />}>
<HeavyComponent />
</Suspense>
</div>
)
}

This pattern enables selective hydration, where critical components hydrate immediately while less important sections load progressively based on visibility or user interaction.

React 18’s streaming SSR represents a significant evolution, enabling progressive HTML delivery through Suspense boundaries:

// Server streaming implementation
import { renderToPipeableStream } from "react-dom/server"
const stream = renderToPipeableStream(<App />, {
onShellReady() {
// Initial shell ready - send immediately
response.statusCode = 200
response.setHeader("content-type", "text/html")
stream.pipe(response)
},
})

Streaming Mechanism: When React encounters a suspended component (e.g., awaiting async data), it immediately sends the HTML shell with placeholders. As Promises resolve, React streams the actual content, which the client seamlessly integrates without full page reloads.

In frameworks like Next.js with the Pages Router, server rendering follows a page-centric data fetching model:

pages/products.js
export async function getServerSideProps({ req, res }) {
const products = await fetchProducts()
// Optional response caching
res.setHeader("Cache-Control", "public, s-maxage=10, stale-while-revalidate=59")
return {
props: { products },
}
}
export default function ProductsPage({ products }) {
return (
<div>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}

This model tightly couples data fetching to routing, with server-side functions executing before component rendering to provide props down the component tree.

SSG shifts rendering to build time, pre-generating static HTML files:

// Build-time static generation
export async function getStaticProps() {
const posts = await fetchPosts()
return {
props: { posts },
revalidate: 3600, // Incremental Static Regeneration
}
}

SSG Performance Benefits:

  • Optimal TTFB: Static files served directly from CDN
  • Aggressive Caching: No server computation at request time
  • Reduced Infrastructure Costs: Minimal server resources required

ISR bridges SSG and SSR by enabling static page updates after build:

export async function getStaticProps() {
return {
props: { data: await fetchData() },
revalidate: 60, // Revalidate every 60 seconds
}
}

ISR Mechanism:

  1. Initial request serves stale static page
  2. Background regeneration triggered if revalidate time exceeded
  3. Subsequent requests serve updated static content
  4. Falls back to SSR on regeneration failure

React Server Components represent an orthogonal concept to traditional SSR, addressing a fundamentally different problem. While SSR optimizes initial page load performance, RSC eliminates client-side JavaScript for non-interactive components.

Key RSC Characteristics:

  • Zero Bundle Impact: Server component code never reaches the client
  • Direct Backend Access: Components can directly query databases and internal services
  • Streaming Native: Naturally integrates with Suspense for progressive rendering

RSC introduces a clear architectural boundary between component types:

// Server Component - runs only on server
export default async function ProductList() {
// Direct database access
const products = await db.query("SELECT * FROM products")
return (
<div>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}

Server Component Constraints:

  • No browser APIs or event handlers
  • Cannot use state or lifecycle hooks
  • Cannot import client-only modules
"use client" // Explicit client boundary marker
import { useState, useEffect } from "react"
export default function InteractiveCart() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>Items: {count}</button>
}

The “use client” directive establishes a client boundary, marking this component and all its imports for inclusion in the client JavaScript bundle.

RSC’s power derives from its sophisticated data protocol that serializes the component tree into a streamable format, often referred to as “progressive JSON” or internally as “Flight”.

The RSC payload contains three primary data types:

  1. Server Component Results: Serialized output of server-executed components
  2. Client Component References: Module IDs and export names for dynamic loading
  3. Serialized Props: JSON-serializable data passed between server and client components
// Example RSC payload structure
{
// Server-rendered content
"1": ["div", {}, "Welcome to our store"],
// Client component reference
"2": ["$", "InteractiveCart", { "initialCount": 0 }],
// Async server component (streaming)
"3": "$Sreact.suspense",
// Resolved async content
"4": ["ProductList", { "products": [...] }]
}

Unlike standard JSON, which requires complete parsing, RSC’s progressive format enables streaming:

  1. Breadth-First Serialization: Server sends UI shell immediately
  2. Placeholder Resolution: Suspended components represented as references (e.g., “$1”)
  3. Progressive Updates: Resolved content streams as tagged chunks
  4. Out-of-Order Processing: Client processes chunks as they arrive, regardless of order
// Progressive streaming example
// Initial shell
"0": ["div", { "className": "app" }, "$1", "$2"]
// Resolved chunk 1
"1": ["header", {}, "Site Header"]
// Resolved chunk 2 (arrives later)
"2": ["main", { "className": "content" }, "$3"]

Server Components integrate deeply with Suspense for coordinated loading states:

import { Suspense } from "react"
export default async function Page() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<AsyncHeader />
</Suspense>
<Suspense fallback={<ContentLoader />}>
<AsyncProductList />
</Suspense>
<InteractiveCartSidebar />
</div>
)
}
async function AsyncHeader() {
const user = await fetchUserData()
return <Header user={user} />
}
async function AsyncProductList() {
const products = await fetchProducts()
return <ProductList products={products} />
}

This pattern transforms the traditional request waterfall into parallel data fetching, with UI streaming as each dependency resolves.

Bundle Size Reduction: Server components contribute zero bytes to client bundles, dramatically reducing Time to Interactive for complex applications.

Reduced Client Computation: Server handles data fetching and rendering logic, sending only final UI descriptions to clients.

Optimized Network Usage: Progressive streaming provides immediate visual feedback while background data loads continue.

Cache-Friendly Architecture: Server component output can be cached at multiple levels—component, route, or application scope.

The modern React ecosystem presents multiple architectural approaches, each optimized for specific use cases:

ArchitectureRendering LocationBundle SizeInteractivitySEOIdeal Use Cases
CSRClient OnlyFull BundleImmediatePoorSPAs, Dashboards
SSRServer + ClientFull BundleDelayed (Hydration)ExcellentDynamic Sites
SSGBuild TimeFull BundleDelayed (Hydration)ExcellentStatic Content
RSC + SSRHybridMinimal BundleSelectiveExcellentModern Apps

React’s architectural evolution follows a clear dependency chain:

Fiber → Concurrency → Suspense → RSC Streaming

  1. Fiber enables interruptible rendering and time-slicing
  2. Concurrency allows pausing and resuming work based on priority
  3. Suspense provides the primitive for waiting on async operations
  4. RSC Streaming leverages Suspense to deliver progressive UI updates

Choose RSC + SSR when:

  • Application requires optimal performance across all metrics
  • Team can manage server infrastructure complexity
  • Application has mix of static and interactive content

Choose Traditional SSR when:

  • Existing SSR infrastructure in place
  • Page-level data fetching patterns sufficient
  • Full client-side hydration acceptable

Choose SSG when:

  • Content changes infrequently
  • Maximum performance required
  • CDN infrastructure available

Choose CSR when:

  • Highly interactive single-page application
  • SEO not critical
  • Simplified deployment requirements

React’s architectural evolution from a simple Virtual DOM abstraction to the sophisticated Fiber-based concurrent rendering system with Server Components represents one of the most significant advances in frontend framework design. The introduction of the Fiber reconciliation engine provided the foundational concurrency primitives that enabled Suspense, which in turn made possible the revolutionary RSC streaming architecture.

This progression demonstrates React’s commitment to solving real-world performance challenges while maintaining its core declarative programming model. The ability to seamlessly compose server and client components within a single React tree, combined with progressive streaming and selective hydration, creates unprecedented opportunities for optimizing both initial page load and interactive performance.

For practitioners architecting modern React applications, understanding these internal mechanisms is crucial for making informed decisions about rendering strategies, performance optimization, and infrastructure requirements. The architectural choices made at the framework level—from Fiber’s double-buffering strategy to RSC’s progressive JSON protocol—directly impact application performance, user experience, and developer productivity.

As the React ecosystem continues to evolve, these foundational architectural patterns will likely influence the broader landscape of user interface frameworks, establishing new paradigms for client-server collaboration in interactive applications.

Tags

Read more

  • Previous in series: React Development & Architecture

    React Hooks

    40 min read

    Master React Hooks’ architectural principles, design patterns, and implementation strategies for building scalable, maintainable applications with functional components.TLDRReact Hooks revolutionized React by enabling functional components to manage state and side effects, replacing class components with a more intuitive, composable architecture.Core PrinciplesCo-location of Logic: Related functionality grouped together instead of scattered across lifecycle methodsClean Reusability: Logic extracted into custom hooks without altering component hierarchySimplified Mental Model: Components become pure functions that map state to UIRules of Hooks: Must be called at top level, only from React functions or custom hooksEssential HooksuseState: Foundation for state management with functional updatesuseReducer: Complex state logic with centralized updates and predictable patternsuseEffect: Synchronization with external systems, side effects, and cleanupuseRef: Imperative escape hatch for DOM references and mutable valuesuseMemo/useCallback: Performance optimization through memoizationPerformance OptimizationStrategic Memoization: Break render cascades, not optimize individual calculationsReferential Equality: Preserve object/function references to prevent unnecessary re-rendersDependency Arrays: Proper dependency management to avoid stale closures and infinite loopsCustom Hooks ArchitectureSingle Responsibility: Each hook does one thing wellComposition Over Monoliths: Compose smaller, focused hooksClear API: Simple, predictable inputs and outputsProduction-Ready Patterns: usePrevious, useDebounce, useFetch with proper error handlingAdvanced PatternsState Machines: Complex state transitions with useReducerEffect Patterns: Synchronization, cleanup, and dependency managementPerformance Monitoring: Profiling and optimization strategiesTesting Strategies: Unit testing hooks in isolationMigration & Best PracticesClass to Function Migration: Systematic approach to converting existing componentsError Boundaries: Proper error handling for hooks-based applicationsTypeScript Integration: Full type safety for hooks and custom hooksPerformance Considerations: When and how to optimize with memoization

  • Next

    React Hooks

    40 min read

    Master React Hooks’ architectural principles, design patterns, and implementation strategies for building scalable, maintainable applications with functional components.TLDRReact Hooks revolutionized React by enabling functional components to manage state and side effects, replacing class components with a more intuitive, composable architecture.Core PrinciplesCo-location of Logic: Related functionality grouped together instead of scattered across lifecycle methodsClean Reusability: Logic extracted into custom hooks without altering component hierarchySimplified Mental Model: Components become pure functions that map state to UIRules of Hooks: Must be called at top level, only from React functions or custom hooksEssential HooksuseState: Foundation for state management with functional updatesuseReducer: Complex state logic with centralized updates and predictable patternsuseEffect: Synchronization with external systems, side effects, and cleanupuseRef: Imperative escape hatch for DOM references and mutable valuesuseMemo/useCallback: Performance optimization through memoizationPerformance OptimizationStrategic Memoization: Break render cascades, not optimize individual calculationsReferential Equality: Preserve object/function references to prevent unnecessary re-rendersDependency Arrays: Proper dependency management to avoid stale closures and infinite loopsCustom Hooks ArchitectureSingle Responsibility: Each hook does one thing wellComposition Over Monoliths: Compose smaller, focused hooksClear API: Simple, predictable inputs and outputsProduction-Ready Patterns: usePrevious, useDebounce, useFetch with proper error handlingAdvanced PatternsState Machines: Complex state transitions with useReducerEffect Patterns: Synchronization, cleanup, and dependency managementPerformance Monitoring: Profiling and optimization strategiesTesting Strategies: Unit testing hooks in isolationMigration & Best PracticesClass to Function Migration: Systematic approach to converting existing componentsError Boundaries: Proper error handling for hooks-based applicationsTypeScript Integration: Full type safety for hooks and custom hooksPerformance Considerations: When and how to optimize with memoization