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:
- Reflow: The browser has to recalculate the layout of elements.
- 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:
- Collect all changes in memory (no direct updates to the real DOM).
- Apply all changes in one batch, minimizing browser reflows and repaints.
- 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>;
}
2. JSX → React.createElement
Babel (the JavaScript compiler) takes that JSX and transforms it into JavaScript like this:
React.createElement('h1', null, 'Hello, world!');
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]
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:
Different element types → replace completely
If a<h1>
becomes a<p>
, React will discard the old<h1>
and create a new<p>
.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.Lists need a
key
prop for efficient matching
When rendering lists, React uses thekey
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>
Now, it changes to:
<h1>Hello, world!</h1>
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>)}
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>)}
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
});
Pitfall 2: Inline Functions Everywhere
If you pass inline functions in JSX, like this:
<button onClick={() => doSomething(value)}>Click</button>
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]);
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.
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).Transitions
WithuseTransition
, you can mark updates as non-urgent. This way, React can delay them to keep the UI fast and snappy.Streaming Server Rendering
For server-side rendered apps, React 19 can stream HTML chunks to the browser as they’re ready, improving load times.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:
- State Changes: How React detects and responds to changes in state.
- Re-rendering: How React decides when and what to re-render.
- Reconciliation: How React determines the minimal set of changes to make to the DOM.
- 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:
- Updates State: The component's state changes based on the new value you set.
- 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.
- 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>
);
}
- When you click the button,
setCount(count + 1)
updates thecount
state. - 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:
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).Find Differences:
React identifies differences between the two trees and marks only the changed parts.Generate Update Instructions:
React creates an update instruction set that tells the browser exactly which elements to change.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>
);
}
When the Add Item
button is clicked:
-
State Updates: The
setItems
function updates the state, adding a new item ('Date'
). - Re-render: The component re-renders with the new list, which includes the new item.
- 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:
State Update:
A user interaction (like clicking a button) triggers a state update (e.g.,setCount
).Re-rendering:
React re-runs the component with the new state, generating new JSX.Virtual DOM Creation:
React creates a new Virtual DOM tree with the updated UI.Diffing and Reconciliation:
React compares the new Virtual DOM tree with the previous one, calculates the differences, and prepares the update instructions.Update the Real DOM:
React applies the minimal set of changes to the real DOM to make the UI match the new Virtual DOM.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>;
});
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>)}
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)
And yet it consistently comes near bottom in performance benchmark tests of popular UI libraries/frameworks.
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 :)