DEV Community

Cover image for How React Achieves High Performance, Even With Extra Layers
Shafiq Ur Rehman
Shafiq Ur Rehman

Posted on

How React Achieves High Performance, Even With Extra Layers

A common interview question around React is:
“If DOM updates are already costly, and React adds Virtual DOM + Reconciliation as extra steps, how can it be faster?”

Many developers, including myself at one point, confidently answer: “Because of Virtual DOM!”
But that’s not the full picture. Let’s break it down properly.

First: How Browser Rendering Works (Brief Overview)

Before we talk about React’s optimizations, let’s first understand how the browser renders things by default.

When the browser receives HTML and CSS from the server, it:

  1. Creates the DOM tree and CSSOM tree
  2. Combines them into the Render Tree
  3. Decides the layout, which element goes where
  4. Finally, paints the pixels on screen

When something changes, like text content or styles, the DOM and CSSOM are rebuilt, the Render Tree is recreated, and then comes the expensive part: Reflow and Repaint.

In this process:

  • Positions are recalculated
  • Elements are repainted wherever styles or content have changed

Both Reflow and Repaint are costly operations, and this is exactly where React tries to help.

Now, let’s move to React and the Virtual DOM.

What Is Virtual DOM?

Virtual DOM is a lightweight copy of the actual DOM, represented as a JavaScript object.

Why was it needed? What was the problem with direct DOM manipulation?

Let’s look at an example.

// Normal DOM manipulation, NOT React
setInterval(() => {
  const span = document.createElement('span');
  span.textContent = new Date().toLocaleTimeString();
  document.getElementById('root').appendChild(span);
}, 1000);
Enter fullscreen mode Exit fullscreen mode

Here, only the span’s time is updating. But if you inspect in DevTools, you’ll see the entire div re-rendering.

Modern browsers are smart; if other elements existed, they wouldn’t repaint them. But even then, in this case, the browser isn’t precise enough. The entire element container is being marked for update.

Now look at the same code in React:

function App() {
  const [time, setTime] = useState(new Date().toLocaleTimeString());

  useEffect(() => {
    const interval = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return (
    <div>
      <h3>Current Time:</h3>
      <span>{time}</span>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here — only the span updates. The div doesn’t re-render. React has already optimized the process at this level.

So how does React do this?

React creates a Virtual DOM.

First, it creates the initial Virtual DOM, a JS object tree mirroring your UI.

Example:

div
├── h3
├── form
│   └── input
└── span → "10:30 AM"
Enter fullscreen mode Exit fullscreen mode

When state updates, say, time changes to “10:31 AM,” React creates a new Virtual DOM tree:

div
├── h3
├── form
│   └── input
└── span → "10:31 AM"
Enter fullscreen mode Exit fullscreen mode

Then React compares the old and new Virtual DOM trees. This comparison process is called Reconciliation.

React sees: “Only the text inside span changed.” So it updates only that span in the Real DOM, and triggers repaint for just that node.

This comparison algorithm is called the Diffing Algorithm.

Two Key Optimizations in Diffing

  1. Batching Updates

If multiple state updates happen, React doesn’t go update the DOM each time. It batches them together and applies them in one go.

Example:

setCount(1);
setName("Alice");
setLoading(true);
Enter fullscreen mode Exit fullscreen mode

React will wait, collect all three, compute a final Virtual DOM, diff it, and update the Real DOM once.

This avoids multiple reflows/repaints.

  1. Element Type Comparison

Let’s say you have a login/logout UI:

function App({ isLoggedIn }) {
  if (isLoggedIn) {
    return <h1>Welcome back, user!</h1>;
  } else {
    return <button>Log In</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Initial Virtual DOM (logged out):

div → class="app"
└── button → "Log In"
Enter fullscreen mode Exit fullscreen mode

Updated Virtual DOM (logged in):

div → class="app"
└── h1 → "Welcome back, user!"
Enter fullscreen mode Exit fullscreen mode

React starts comparing from the root.

  • div → same → check props → class="app" → same → move on
  • Now children: button vs h1 → TYPE MISMATCH

React doesn’t try to “update” the button into an h1. It destroys the entire subtree and recreates it from scratch.

This is efficient because trying to morph one element into another is more expensive than just replacing it.

So far, this process seems optimized. Then why did React introduce Fiber?

Why Was Fiber Needed?

The original Reconciliation process had a critical flaw: It was synchronous and recursive.

Once started, it would run to completion, blocking the main thread.

Imagine this scenario:

  • User is typing in an input field
  • Meanwhile, 10 API calls return and trigger UI updates

Because React’s diffing was synchronous, it would process all 10 updates in one blocking pass, freezing the UI while the user is typing.

React had no way to say: “This user input is high priority, do it first. Those API updates? Do them later.”

Everything was treated equally and executed in one uninterrupted stack.

This hurt user experience.

So in React 16, Fiber was introduced.

What Is React Fiber?

React Fiber is a new Reconciliation algorithm. All updates in modern React go through Fiber.

Fiber solved the core problem: It made Reconciliation interruptible, prioritizable, and asynchronous.

Let’s understand how.

Fiber Working, Step by Step

Consider this component:

function App() {
  const [name, setName] = useState("");
  const [loading, setLoading] = useState(false);

  return (
    <div className="app">
      <h2 onClick={() => {
        setName("Sofia");
        startTransition(() => {
          setLoading(true);
        });
      }}>
        Click to Update
      </h2>
      <Profile name={name} />
      <Dashboard loading={loading} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • setName("Sofia") → high priority update (Sync Lane)
  • setLoading(true) wrapped in startTransition → low priority (Transition Lane)

Fiber will handle them differently.

Fiber Architecture, Key Concepts

1. Fiber Node

Every element, component, DOM node, and text becomes a Fiber Node.

Example component tree:

<App>
  ├── <h2>
  ├── <Profile>
  └── <Dashboard>
Enter fullscreen mode Exit fullscreen mode

Becomes a Fiber Tree where each node is a unit of work.

2. Current Tree vs Work-In-Progress Tree

  • Current Tree → The tree currently rendered on screen
  • Work-In-Progress (WIP) Tree → The tree being prepared for next render

When updates happen, React builds the WIP tree and then swaps it with the Current Tree during the Commit Phase.

Fiber Reconciliation Two Phases

Phase 1: Render Phase (Interruptible)

This phase has two sub-phases:

a. Begin Work

React visits each Fiber Node starting from the root.

It checks:

  • Does this node need update?
  • What’s the new state/props?
  • Create/clone Fiber Node for WIP tree

b. Complete Work

After a node’s children are processed, React:

  • Creates the actual DOM node (if new)
  • Links it to the Fiber Node via stateNode
  • Adds the Fiber Node to the “Effect List” if it needs a DOM update

Example:

fiber.stateNode = document.createElement('h2');
Enter fullscreen mode Exit fullscreen mode

The Effect List is a linked list of nodes that need DOM mutations.

Traversal Order, Depth First

Fiber doesn’t use recursion; it uses a linked list with pointers:

  • child → first child
  • sibling → next sibling
  • return → parent

Traversal order:

  1. Start at Root
  2. Go to the child
  3. Keep going to the child until the leaf
  4. At leaf → go to sibling
  5. If no sibling → go back to parent
  6. Parents’ sibling? Go there. Otherwise, go to the grandparent.

Example:

Root
└── App
    ├── h2
    ├── Profile
    └── Dashboard
Enter fullscreen mode Exit fullscreen mode

Traversal:

Root → App → h2 (leaf) → Profile (sibling) → Dashboard (sibling) → App (parent) → Root

At each node, Begin Work → then, after children → Complete Work.

Phase 2: Commit Phase (Synchronous)

Once the Render Phase is done, React has:

  • A complete WIP Fiber Tree
  • An Effect List — with all nodes needing DOM updates

Now, React enters the Commit Phase — which is synchronous and uninterruptible.

It walks the Effect List — and performs:

  • Insertions
  • Updates
  • Deletions

On the Real DOM.

Then, it swaps WIP Tree → becomes new Current Tree.

Update Phase, How Priorities Work

When state updates:

  1. React creates an Update Object → { payload, timestamp, lane }
  2. Enqueues it in the component’s update queue
  3. Marks the Fiber Node (and all ancestors) as “needing work”
  4. Schedules the update based on lane (priority)

Example:

// High priority
setName("Sofia"); // Sync Lane

// Low priority
startTransition(() => {
  setLoading(true); // Transition Lane
});
Enter fullscreen mode Exit fullscreen mode

React’s Scheduler:

  • Checks which updates are pending
  • Assigns priority: Sync, Transition, Idle
  • Executes high-priority first

So when you click the button:

  1. React creates a WIP(work-in-progress) tree
  2. Processes setName("Sofia") → updates Profile
  3. Skips setLoading(true) for now (low priority)
  4. Commits → UI updates immediately
  5. Later — starts new WIP tree → processes setLoading(true) → commits Dashboard update

The user sees instant feedback, and background work occurs later.

Fiber’s Real Power

  • Work is split into chunks → doesn’t block the main thread
  • High-priority work (user input) jumps the queue
  • Low priority work (data loading) waits — but doesn’t block
  • Browser gets breathing room → stays responsive

Even though Fiber adds more steps, it makes the right steps happen at the right time.

Top comments (0)