DEV Community

Cover image for The Virtual DOM: React’s Invisible Engine Room
Ali Aslam
Ali Aslam

Posted on • Edited on

The Virtual DOM: React’s Invisible Engine Room

The term Virtual DOM might sound mysterious, like React is using some secret web browser behind the scenes to make everything faster. But in reality, the Virtual DOM is pretty simple — it's a JavaScript data structure that helps make your UI updates fast, efficient, and less error-prone.

In the previous article of our React deep dive series, we explored JSX — the syntax that lets you write HTML-like code inside JavaScript. But JSX isn’t where the magic ends, that's where it begins.

In this section, we’ll go beyond the buzzwords and explain:

  • What the Virtual DOM actually is.

  • Why React doesn’t update the real DOM directly.

  • How diffing and reconciliation work.

  • How React 19’s concurrency model takes things up a notch.

  • Common performance pitfalls and how to avoid them.

Let’s dive into the invisible engine behind React’s fast performance!


What Exactly Is the Virtual DOM?

The Virtual DOM (VDOM) is a lightweight in-memory representation of your UI.

Here’s the breakdown:

  • It's a tree of JavaScript objects that describes the current state of your UI.
  • React creates this tree every time your components re-render.
  • It compares this tree to the previous one and figures out the smallest set of changes to update the real DOM.

🧠 Analogy
Imagine the real DOM is a garden. If you were to update it directly each time, you’d have to go in and replace flowers one by one — each time you think they might need changing.

But with the Virtual DOM, you first take a photo of the garden, mark the changes you want to make on the photo, and then go into the garden just once to update only those specific flowers. Much more efficient, right?


Why Not Just Use the Real DOM?

The real DOM can be slow and expensive. Every time you update it, there’s a lot of extra work happening under the hood:

  1. Reflow: The browser has to recalculate the layout of elements.
  2. Repaint: The browser has to redraw the elements on the screen.

If your app is constantly updating the real DOM (let’s say 50 times in a row), you end up running layout calculations 50 times. Yikes! This leads to poor performance.

Enter the Virtual DOM:

React’s Virtual DOM allows it to:

  1. Collect all changes in memory (no direct updates to the real DOM).
  2. Apply all changes in one batch, minimizing browser reflows and repaints.
  3. Avoid unnecessary DOM operations, making the UI updates faster and smoother.

The Rendering Flow (Step-by-Step)

Let’s walk through the process of how React uses the Virtual DOM to efficiently update the UI. It’s actually pretty cool when you see how it all fits together.

1. You Write JSX

function App() {
  return <h1>Hello, world!</h1>;
}
Enter fullscreen mode Exit fullscreen mode

2. JSX → React.createElement

Babel (the JavaScript compiler) takes that JSX and transforms it into JavaScript like this:

React.createElement('h1', null, 'Hello, world!');
Enter fullscreen mode Exit fullscreen mode

This React element is just a JavaScript object that describes:

  • The type of element ('h1').
  • The props (empty in this case).
  • The children (the text "Hello, world!").

3. React Builds the Virtual DOM Tree

React collects all the React elements into a tree structure that represents your entire UI.

This tree is virtual because it lives only in memory — it’s not yet reflected in the real DOM.

4. React Compares Old & New Trees (Diffing)

Here’s where the magic happens. When something changes in your app (like a button click or state update), React compares the old Virtual DOM tree to the new one:

  • If something has changed, React will generate an update instruction.
  • This is known as diffing — finding the minimal set of changes between the old and new trees.

5. React Updates the Real DOM

After diffing, React figures out the minimal set of changes it needs to apply to the real DOM. It only updates what’s changed and leaves everything else alone. This results in fast and efficient rendering.


📷 Rendering Flow Visual (Described)

[JSX Code]
   ↓ (compile)
[React Elements]
   ↓ (build tree)
[Virtual DOM Tree]
   ↓ (diff)
[Change Instructions]
   ↓ (apply)
[Real DOM in Browser]
Enter fullscreen mode Exit fullscreen mode

How Diffing Works in Practice

When React compares the old and new Virtual DOM trees, it uses an optimized diffing algorithm. If we tried to use a naive approach, it would take too long — imagine comparing every single node with every other node.

Instead, React makes a few smart assumptions to speed things up:

  1. Different element types → replace completely
    If a <h1> becomes a <p>, React will discard the old <h1> and create a new <p>.

  2. Same element type → compare props and children
    If the element type hasn’t changed (like from <h1> to <h1>), React compares the children (or props) and only updates what’s necessary.

  3. Lists need a key prop for efficient matching
    When rendering lists, React uses the key prop to uniquely identify elements, making the matching process much faster.

Example: Efficient Text Update

Let’s say we have an <h1> element with this content:

<h1>Hello</h1>
Enter fullscreen mode Exit fullscreen mode

Now, it changes to:

<h1>Hello, world!</h1>
Enter fullscreen mode Exit fullscreen mode

React sees:

  • Same element type: <h1>.
  • Props haven’t changed.
  • Only the text has changed, so React only updates the text node.

Example: List Updates With Keys

Without keys:

{['A', 'B', 'C'].map(item => <li>{item}</li>)}
Enter fullscreen mode Exit fullscreen mode

Here, React has no way of identifying each <li> element individually, so if the list changes order, it might have to destroy and recreate DOM nodes unnecessarily.

With keys:

{items.map(item => <li key={item.id}>{item.name}</li>)}
Enter fullscreen mode Exit fullscreen mode

By adding a key to each item, React can efficiently match items in the list and only move around the elements that actually changed.


⚠️ Edge Case:

Using array indexes as keys (key={index}) might seem like a quick fix, but it’s not recommended. This can cause issues in dynamic lists (e.g., input fields losing focus or animations glitching).


Performance Pitfalls & Fixes

While React’s Virtual DOM makes things fast, you can still run into performance issues if you’re not careful. Here are some common pitfalls and how to fix them.

Pitfall 1: Unnecessary Re-renders

If a parent component re-renders, all of its children re-render by default — even if their props haven’t changed. This can be costly in complex apps.

Fix: Use React.memo for pure components:

const Child = React.memo(function Child(props) {
  // renders only if props change
});
Enter fullscreen mode Exit fullscreen mode

Pitfall 2: Inline Functions Everywhere

If you pass inline functions in JSX, like this:

<button onClick={() => doSomething(value)}>Click</button>
Enter fullscreen mode Exit fullscreen mode

A new function is created on every render, which can slow down your app.

Fix: Use useCallback to stabilize the function reference:

const handleClick = useCallback(() => doSomething(value), [value]);
Enter fullscreen mode Exit fullscreen mode

Pitfall 3: Big Lists Without Virtualization

If you’re rendering thousands of DOM elements at once, it can slow down the browser.

Fix: Use virtualization libraries like react-window or react-virtualized to only render the elements that are visible on screen.


React 19 and Concurrency

React 19 comes with exciting new concurrency features that make rendering even more efficient and responsive.

  1. Interruptible Rendering
    React can pause rendering work for non-urgent updates (like loading an image) to prioritize more important ones (like typing in a text field).

  2. Transitions
    With useTransition, you can mark updates as non-urgent. This way, React can delay them to keep the UI fast and snappy.

  3. Streaming Server Rendering
    For server-side rendered apps, React 19 can stream HTML chunks to the browser as they’re ready, improving load times.

  4. Selective Hydration
    React 19 can hydrate only the parts of the page that the user interacts with first, making the page feel interactive sooner.


How React Renders UI: The Full Rendering Pipeline

So far, we’ve learned about JSX and how it turns into JavaScript, and we’ve explored the Virtual DOM and how React uses it to efficiently update the UI. Now, it’s time to bring everything together and understand how React takes your code (JSX + JavaScript), updates the state, and renders UI changes to the screen.

In this section, we’re going to break down React’s rendering process step-by-step:

  1. State Changes: How React detects and responds to changes in state.
  2. Re-rendering: How React decides when and what to re-render.
  3. Reconciliation: How React determines the minimal set of changes to make to the DOM.
  4. Rendering Pipeline: The full lifecycle, from data changes to DOM updates.

Let’s dive into the details!


Step 1: State Changes in React

In React, state is the data that determines how your components look and behave. When state changes, React triggers a re-render to reflect the updates in the UI.

What Happens When State Changes?

When you call the setState() function (or use the useState hook), React goes through several steps:

  1. Updates State: The component's state changes based on the new value you set.
  2. Triggers a Re-render: React marks the component as “dirty” (needing a re-render) and schedules an update. It doesn't immediately re-render, though; React batches updates to optimize performance.
  3. Runs the Component Again: React calls the component function again with the new state, generating the new JSX.

Example: State Update

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
  1. When you click the button, setCount(count + 1) updates the count state.
  2. React queues a re-render of the Counter component, and the new state (count + 1) is used to update the <h1> tag.

Step 2: Re-rendering — What React Does When State Changes

When a component re-renders, React follows these steps:

1. Check for Changes:

React checks if the component's state or props have changed. If not, React skips the re-rendering of that component and its children.

2. Rebuild JSX:

React runs your component again, using the new state, and generates new JSX.

3. Create New Virtual DOM Tree:

React creates a new Virtual DOM tree that represents the updated UI.

4. Diffing:

React compares the old Virtual DOM tree to the new one. This is where the diffing algorithm comes into play. It determines the minimal set of changes needed and creates an update plan.


Step 3: Reconciliation — The Smart Way React Updates the DOM

Reconciliation is React’s process of efficiently updating the real DOM. This process ensures that React only updates the parts of the UI that need to change, without disturbing the rest of the UI.

Here’s how reconciliation works:

  1. Compare Old and New Virtual DOM:
    React compares the old Virtual DOM to the new Virtual DOM tree (which was generated during the re-rendering phase).

  2. Find Differences:
    React identifies differences between the two trees and marks only the changed parts.

  3. Generate Update Instructions:
    React creates an update instruction set that tells the browser exactly which elements to change.

  4. Apply Changes to the Real DOM:
    React updates the real DOM using the instructions generated during the diffing process. These updates happen in batches, reducing the performance cost.


Example: Reconciliation in Action

Consider this example where the list changes based on state:

function ItemList() {
  const [items, setItems] = useState(['Apple', 'Banana', 'Cherry']);

  const addItem = () => setItems([...items, 'Date']);

  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <ul>
        {items.map((item, index) => <li key={index}>{item}</li>)}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

When the Add Item button is clicked:

  1. State Updates: The setItems function updates the state, adding a new item ('Date').
  2. Re-render: The component re-renders with the new list, which includes the new item.
  3. Reconciliation: React compares the old list with the new list, recognizes that only one new item was added, and efficiently updates the DOM to show the new item without touching the rest of the list.

Step 4: Rendering Pipeline — How React Brings It All Together

Now that we know how state updates, re-renders, and reconciliation work, let’s look at the full rendering pipeline from start to finish:

  1. State Update:
    A user interaction (like clicking a button) triggers a state update (e.g., setCount).

  2. Re-rendering:
    React re-runs the component with the new state, generating new JSX.

  3. Virtual DOM Creation:
    React creates a new Virtual DOM tree with the updated UI.

  4. Diffing and Reconciliation:
    React compares the new Virtual DOM tree with the previous one, calculates the differences, and prepares the update instructions.

  5. Update the Real DOM:
    React applies the minimal set of changes to the real DOM to make the UI match the new Virtual DOM.

  6. Repaint:
    Finally, the browser renders the updated UI, and you see the changes instantly.


⚠️ React 19 and Concurrent Rendering

With React 19, this process has become even more efficient thanks to concurrent rendering. React can now prioritize important updates over less critical ones.

For example, if you're typing in a text box while an image is loading, React can pause rendering the image and continue with the text box updates, making sure the UI feels snappy and responsive.

This interruptible rendering is part of React 19’s concurrent rendering improvements, making the entire process smoother than ever.


Performance Optimization Tips

While React’s rendering pipeline is already optimized, there are a few things you can do to make your app even faster:

1. Memoize Components with React.memo

If a component’s output is based solely on props, you can prevent unnecessary re-renders by wrapping it in React.memo:

const MemoizedComponent = React.memo(function MyComponent(props) {
  return <div>{props.text}</div>;
});
Enter fullscreen mode Exit fullscreen mode

This tells React to skip rendering the component unless the props have changed.

2. Optimize List Rendering with key Prop

For dynamic lists, always provide a unique key prop to help React efficiently identify and update list items:

{items.map(item => <li key={item.id}>{item.name}</li>)}
Enter fullscreen mode Exit fullscreen mode

Using the key prop allows React to track changes in the list and avoid unnecessary DOM operations.

3. Use Lazy Loading and Code Splitting

React 19 introduces Suspense for code splitting. You can dynamically load parts of your app as needed, reducing initial load time.

Wrapping Up the Virtual DOM

The Virtual DOM is one of the key features that makes React so fast and efficient. By keeping a lightweight in-memory representation of the real DOM, React can make smart, minimal updates to the UI — ensuring that your app stays responsive and performant.

Key Takeaways:

  • The Virtual DOM allows React to update only the parts of the UI that need to change, minimizing the need for costly DOM manipulations.
  • React compares the old and new Virtual DOMs in a process called diffing to figure out what needs to be updated.
  • By batching updates and using reconciliation, React ensures that UI changes are applied efficiently.
  • Code splitting and lazy loading in React 19 make your apps even more efficient by only loading what's needed, reducing initial load times.

What’s Next?

Now that you understand how React optimizes UI updates with the Virtual DOM, it’s time to dive into the building blocks of every React app: components and props.
In the next article, you'll learn how to:

  • Create reusable functional and class components

  • Pass data using props

  • Understand the component hierarchy and how React renders them

  • Conditional and dynamic rendering

By mastering components and props, you'll be well on your way to building dynamic, scalable user interfaces in React. Later' we'll explore how it all ties up to UI and virtual DOM.

React Components, Props, and JSX: A Beginner’s Guide →


Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Want video demos? Subscribe on YouTube: @LearnAwesome

Top comments (2)

Collapse
 
jonrandy profile image
Jon Randy 🎖️

...that makes React so fast and efficient.

And yet it consistently comes near bottom in performance benchmark tests of popular UI libraries/frameworks.

Collapse
 
a1guy profile image
Ali Aslam

Thanks for pointing that out — it’s a valuable point for readers to be aware of.

You are right! React lags behind newer frameworks in certain benchmark tests. I have added a performance reality check section in 'Why learn react in 2025' article of this series to cover that exact topic. It has links to benchmark results and a discussion on why React remains relevant despite underperforming in benchmark tests.

Here's link directly to that section
React Performance reality check

Please have a look. Hopefully that would address what you had in mind. I'd love to hear your thoughts :)