Skip to content

Latest commit

 

History

History
396 lines (281 loc) · 21.3 KB

File metadata and controls

396 lines (281 loc) · 21.3 KB

Rendering, Reconciliation, Compilation, & Fiber

To truly master React, you need to look past the components and hooks and understand the engine underneath. The four concepts you asked about—Rendering, Reconciliation, Fiber, and Compilation—are the exact sequence of how React takes your code and puts pixels on the screen, and how it evolved to do that blazingly fast.

Let's break them down from zero to hero. Note: I will explain Fiber (D) before Compilation (C), because Fiber is the engine that drives modern React, and Compilation is the newest evolution that optimizes that engine.

(A) React Rendering

The Misconception: Most developers think "rendering" means painting pixels to the browser screen. The Reality: In React, rendering simply means calling your component functions to figure out what the UI should look like. Updating the screen is a separate phase called "Committing."

The Three-Step Pipeline

Whenever state changes, React goes through a strict pipeline:

  1. TRIGGER            2. RENDER                 3. COMMIT
  (State changed)       (Call the function)       (Update the DOM)
  
  +-----------+         +-----------------+       +----------------+
  | setCount  | ------> | function App()  | ----> | document.      |
  | (User     |         | returns <div/>  |       | appendChild()  |
  |  clicks)  |         | (Calculations)  |       | (DOM Mutation) |
  +-----------+         +-----------------+       +----------------+

What happens during the Render Phase?

When React calls your component function, the JSX you write is converted into a plain JavaScript object called a React Element. This object describes the UI.

// 1. What you write (JSX):
function Button() {
  return <button className="btn">Click</button>;
}

// 2. What it compiles to behind the scenes:
function Button() {
  return React.createElement('button', { className: 'btn' }, 'Click');
}

// 3. What the "Render" phase actually produces (A plain JS Object):
const reactElement = {
  type: 'button',
  props: {
    className: 'btn',
    children: 'Click'
  }
};

During rendering, React builds a massive tree of these plain JavaScript objects (this is the famous Virtual DOM). It does not touch the browser's real DOM yet.

(B) React Reconciliation

Once the Render phase generates the new Virtual DOM tree of objects, React needs to figure out what changed between the old tree (from the last render) and the new tree.

This process of comparing the two trees and calculating the exact differences is called Reconciliation.

The Diffing Algorithm

Comparing two completely different trees in computer science normally takes O(n^3) time (where n is the number of elements). For an app with 1,000 elements, that's 1 billion operations. Too slow.

React's Reconciler uses a heuristic O(n) algorithm based on two simple rules:

  1. Different Element Types = Nuke it. If a <div> turns into a <section>, React destroys the old DOM node and all its children, and builds a fresh one.
  2. Same Element Type = Update Props. If a <div className="red"> turns into <div className="blue">, React keeps the exact same DOM node and just flips the class name.

Visualizing Reconciliation

      OLD VIRTUAL DOM TREE                 NEW VIRTUAL DOM TREE
      
           <App>                                <App>
             |                                    |
         <Header>                             <Header>  (Same type, keep DOM)
        /        \                           /        \
   <Title>     <Menu>                   <Title>     <Menu>
  "Home"      (isOpen: false)          "Home"      (isOpen: true) -> Update Prop!
                 |                                    |
               <null>                             <Dropdown> -> Create new DOM!

The "Gotcha" of Reconciliation: List Keys

If you render a list of items and add a new item to the top of the list, React gets confused. It compares item 1 to item 1, sees they are different, and mutates it. It does this for the whole list, resulting in terrible performance.

// BAD: React mutates every single DOM node if you add an item to the top.
const items = ['Apple', 'Banana']; 
items.map(item => <li>{item}</li>);

// GOOD: The 'key' tells the Reconciler: "Hey, this is the exact same 
// item as before, just move its physical DOM node down!"
items.map(item => <li key={item.id}>{item.name}</li>);

(D) What was/is React Fiber?

History: Before React 16 (2017), React used the "Stack Reconciler." It processed the Virtual DOM tree synchronously, using the browser's JavaScript call stack. The Problem: If you had a massive component tree, React would start reconciling at the top and could not stop until it reached the bottom. If this took 100 milliseconds, the browser froze. You couldn't type, click, or see animations (dropped frames).

The Solution: In React 16, the core team completely rewrote the engine. They called it React Fiber.

What is a Fiber?

A "Fiber" is just a JavaScript object. It represents a single "unit of work" for a component.

Instead of relying on the JS call stack (which cannot be paused), React Fiber builds its own singly-linked list tree. Because it's a data structure instead of a recursive function call, React can pause, abort, or prioritize rendering work.

The Fiber Tree Architecture

Every Fiber node points to its first child, its next sibling, and its parent (return).

    +-------------+
    | Fiber: App  |
    +-------------+
      | (child)  ^
      v          | (return)
    +-------------+    (sibling)    +-------------+
    | Fiber: Nav  | --------------> | Fiber: Main |
    +-------------+                 +-------------+
                                      | (child)  ^
                                      v          | (return)
                                    +-------------+
                                    | Fiber: Post |
                                    +-------------+

How Fiber enables Concurrency (Time Slicing)

Because work is broken into these little "Fiber" objects, React can act like an operating system:

  1. React works on Fiber: Nav.
  2. React checks: "Does the browser need to paint an animation or handle a user click right now?"
  3. If yes, React pauses its rendering work and yields control to the browser.
  4. When the browser is idle, React picks up exactly where it left off at Fiber: Main.

This makes modern React apps feel instantly responsive, even if they are doing heavy calculations in the background (using features like useTransition).

(C) React Compilation (The Modern Era)

History: With Fiber making rendering fast, developers hit a new problem: Over-rendering. If a parent component's state changed, Fiber would rapidly re-render all of its children. To stop this, developers had to manually wrap everything in React.memo, useMemo, and useCallback. This led to ugly, unreadable code.

The Solution: Released with React 19 (2024), the React Compiler (originally codenamed "React Forget") shifts the burden from the developer to the build tool.

What is the React Compiler?

It is not a runtime library. It is a build-time compiler (like Babel or SWC) that analyzes your JavaScript code before it runs in the browser. It automatically injects memoization logic under the hood.

Visualizing the Compiler's Job

  1. Your Clean Code         2. Build Step          3. What Browser Actually Runs
  (No manual memoization)    (React Compiler)       (Heavily optimized output)
  
  +-------------------+      +--------------+       +-------------------------+
  | function App() {  |      |  Analyzes    |       | function App() {        |
  |   const data = [1]| ===> |  AST (Code   | ====> |   const $ = _c(2);      |
  |   return <List /> |      |  Structure)  |       |   if ($[0] !== deps) {  |
  | }                 |      +--------------+       |      $[1] = <List />    |
  +-------------------+                             |   }                     |
                                                    |   return $[1];          |
                                                    +-------------------------+

The Code Transformation

The Compiler looks for values that haven't changed and caches them automatically.

Before the Compiler (The manual way):

function ProductPage({ productId, theme }) {
  // We had to manually tell React not to recreate this object 
  // unless productId changed, so the <ReviewList> wouldn't re-render.
  const fetchOptions = useMemo(() => {
    return { id: productId, limit: 10 };
  }, [productId]);

  return <ReviewList options={fetchOptions} theme={theme} />;
}

With the React Compiler (The modern way):

function ProductPage({ productId, theme }) {
  // You just write normal JavaScript. 
  const fetchOptions = { id: productId, limit: 10 };

  // The compiler analyzes this at build time. It sees that 'fetchOptions' 
  // only depends on 'productId'. It automatically caches the object and 
  // the <ReviewList> UI block behind the scenes.
  return <ReviewList options={fetchOptions} theme={theme} />;
}

Summary of the Entire Journey

  1. Compilation (Build Time): Your clean React code is automatically optimized and cached by the React Compiler.
  2. Rendering (Runtime): State changes. React calls your component functions to create a new Virtual DOM (React Elements).
  3. Reconciliation (Runtime): React compares the new Virtual DOM against the old one.
  4. Fiber (The Engine): The engine orchestrating all of this, slicing the work into chunks and pausing for the browser so the app never freezes.
  5. Committing (The End): React takes the exact differences found by Reconciliation and updates the browser's actual DOM.

Virtual DOM in-depth

To truly understand the Virtual DOM (VDOM), we have to strip away the magic of React and look at it as pure software engineering. As a senior engineer, you know that adding an abstraction layer usually adds overhead. So why did React add an entire fake DOM layer, and how does it actually make things faster?

Here is the deep dive into the Virtual DOM, followed by the trickiest interview questions on the topic.

1. The Problem: The Real DOM is Heavy

The real Document Object Model (DOM) is an API provided by the browser. When you create a <div> in the real DOM, you aren't just creating a simple object. You are creating a massive C++ object inside the browser engine that carries hundreds of properties, event listeners, and layout computations.

Every time you change the real DOM, the browser has to do heavy lifting:

  1. Recalculate CSS styles
  2. Compute Layout (Reflow) -> Where does this box go? Did it push other boxes?
  3. Repaint -> Draw the pixels on the screen

If you have a list of 50 items and you update the 1st one, naive DOM manipulation might force the browser to recalculate the layout for all 50 items. This is called layout thrashing, and it destroys performance.

2. The Solution: The Virtual DOM

The Virtual DOM is not a browser feature. It is a design pattern. It is simply a lightweight, plain JavaScript object that represents what the real DOM should look like.

Because it is just a JS object in memory, creating, updating, and destroying it is extremely cheap and fast. It requires zero CSS recalculations, zero layouts, and zero painting.

Visualizing the Difference

       REAL DOM NODE (Heavy)                  VIRTUAL DOM NODE (Lightweight JS Object)
       +-----------------------+              +-----------------------+
       | HTMLDivElement        |              | const vNode = {       |
       | - clientHeight: 100   |              |   type: 'div',        |
       | - clientWidth: 200    |  ========>   |   props: {            |
       | - innerHTML: "..."    |  Abstracted  |     id: 'box',        |
       | - onmousemove: fn()   |              |     children: 'Hello' |
       | - style: CSSStyleDecl |              |   }                   |
       | ... (400+ properties) |              | }                     |
       +-----------------------+              +-----------------------+

3. The Double-Buffering Mechanics

React doesn't just have one Virtual DOM. Under the hood, it maintains two trees to process updates. This is inspired by the "double buffering" technique used in video game rendering to prevent screen tearing.

  1. The Current Tree: Represents what is currently visible on the screen.
  2. The Work-in-Progress (WIP) Tree: Built in the background when a state change occurs.

The Lifecycle of an Update

  [State Changes in App]
           |
           v
  1. RENDER PHASE (Build the WIP Tree)
  React calls your component functions. It generates a brand new 
  Virtual DOM tree in memory.
  
       Current Tree                 Work-In-Progress (WIP) Tree
       +--------+                   +--------+
       |  div   |                   |  div   |
       +--------+                   +--------+
         |    |                       |    |
      +----+ +----+                +----+ +-------+
      | h1 | | p  |                | h1 | | input | <-- New element!
      +----+ +----+                +----+ +-------+
           |
           v
  2. DIFFING PHASE (Reconciliation)
  React compares the Current Tree and the WIP Tree memory objects.
  It calculates the exact mathematical difference (the "diff").
  "Ah, the h1 is exactly the same, but I need to replace the <p> with an <input>."
           |
           v
  3. COMMIT PHASE (Patching the Real DOM)
  React takes the computed differences and applies them to the REAL DOM 
  in a single, batched operation. 

       Real Browser DOM
       document.replaceChild(inputNode, pNode);

4. The Diffing Algorithm (Heuristics)

Comparing two trees of 'n' nodes to find the minimum number of operations to transform one into the other is an O(n^3) problem. For 1,000 nodes, that is 1 billion operations.

React optimizes this to an O(n) algorithm using two assumptions:

Rule 1: Level-by-Level Comparison React only compares nodes at the exact same level in the tree. It does not look across different branches.

     Tree A                  Tree B
       [Root]                  [Root]
      /      \                /      \
    [A]      [B]            [A]      [C]   <-- React compares A to A, and B to C.
      \                        \           <-- It will NOT realize B moved here. 
      [C]                      [B]         <-- It will destroy the old B and create a new B.

Rule 2: Element Types dictate survival

  • Different Types: If a <div> becomes a <span>, React instantly destroys the <div>, throws away all its children, and mounts a brand new <span>. It doesn't even bother diffing the children.
  • Same Types: If a <div className="red"> becomes <div className="blue">, React keeps the exact same real DOM node and simply updates the class attribute.

5. Tricky Interview Gotchas & Questions

Here are the advanced questions that separate the juniors from the seniors in frontend architecture interviews.

Q1. "Is the Virtual DOM faster than the Real DOM?"

The Gotcha: A naive candidate will say "Yes, the Real DOM is slow, the VDOM is fast." The Hero Answer: "No. The Virtual DOM is pure overhead. The absolute fastest way to update the screen is to use vanilla JavaScript to target a specific DOM node and update it directly. However, writing imperative vanilla JS for complex apps is unmaintainable. The Virtual DOM is a compromise: it allows us to write declarative UI code (which is developer-friendly) while automatically batching and optimizing the underlying real DOM updates to be almost as fast as hand-optimized vanilla JS."

Q2. "What happens if you use the array index as a key in a mapped list, and then you delete the first item?"

The Gotcha: Understanding how keys actually map VDOM nodes to Real DOM nodes. The Hero Answer: "It causes a massive bug and ruins performance. If you have a list of items ['A', 'B', 'C'] with keys 0, 1, 2 and you delete 'A', the new array is ['B', 'C'] with keys 0, 1. React's diffing algorithm looks at key 0. It sees it used to be 'A' and is now 'B'. Instead of just removing the first DOM node, React will mutate the text of the first node from A to B, mutate the second node from B to C, and delete the third node. This causes unnecessary DOM updates and breaks local component state (like uncontrolled inputs) because React attached the state to the index, not the data."

// BUG SCENARIO
const list = ['A', 'B', 'C'];
// Render 1: <li key="0">A</li>, <li key="1">B</li>, <li key="2">C</li>

// Delete 'A'. New list is ['B', 'C']
// Render 2: <li key="0">B</li>, <li key="1">C</li>

// React sees key="0" survived. It reuses the real DOM node for 'A', 
// but forces its text to become 'B'. 

Q3. "If a parent component's state changes, do all its children re-render in the Virtual DOM? Does the Real DOM update for all of them?"

The Gotcha: Confusing the Render Phase with the Commit Phase. The Hero Answer: "By default, yes, the Virtual DOM will re-render (re-execute the functions) for the parent and all of its descendants. It creates a brand new VDOM tree for that entire branch. However, the Real DOM will only update the specific nodes where the new VDOM differs from the old VDOM. So, VDOM calculations happen for the whole branch, but Real DOM mutations are strictly limited to the changed pixels." (Note: You can prevent the VDOM from re-rendering unchanged children using React.memo).

Q4. "What is the difference between the Shadow DOM and the Virtual DOM?"

The Gotcha: They sound similar but have zero relationship to each other. The Hero Answer: "The Virtual DOM is a JavaScript pattern used by React to optimize rendering. It lives in memory. The Shadow DOM is an official browser API (part of Web Components) used to encapsulate CSS and markup so they don't leak out and affect the rest of the page. You can inspect the Shadow DOM in Chrome DevTools; you cannot inspect the Virtual DOM there because it's just a JS variable."

Q5. "Do all modern frameworks use a Virtual DOM? How does Svelte do it?"

The Gotcha: Assuming VDOM is the only way to build declarative UIs. The Hero Answer: "No. Frameworks like Svelte and SolidJS do not use a Virtual DOM. Instead of figuring out what changed at runtime by diffing two trees, they use a compiler to figure out what could change at build time. When you write state in Svelte, the compiler generates vanilla JavaScript that directly targets and updates the exact real DOM node when that specific state changes. This removes the memory and CPU overhead of building and diffing VDOM trees entirely."