
Introduction to DOM and Browser Rendering
Before diving into our topic React DOM and Virtual DOM, it’s important to understand what the DOM is and how browsers render web pages.
What is the DOM?
The Document Object Model (DOM) is a programming interface for web documents. It represents the page as a tree of nodes that browsers use to render the page. Each HTML element becomes a node in this tree.
For example, this HTML:
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<div id="container">
<h1>Hello World</h1>
<p>This is a paragraph.</p>
</div>
</body>
</html>
Creates this DOM tree:
- html
 - head
  - title: "My Page"
 - body
  - div#container
   - h1: "Hello World"
   - p: "This is a paragraph."
How Browsers Render Pages
When a browser loads a page, it follows these steps:
- Parse HTML to construct the DOM tree
- Parse CSS to construct the CSSOM (CSS Object Model) tree
- Combine DOM and CSSOM to create a render tree
- Layout (or reflow) to compute the exact position and size of each element
- Paint the pixels to the screen
This process is computationally expensive, especially the layout and paint steps. When JavaScript changes the DOM, the browser often needs to recalculate styles, perform layout, and repaint, which can lead to performance issues.
The Problem: DOM Manipulation is Expensive
Direct DOM manipulation can be slow for several reasons:
- Layout thrashing: Repeatedly reading and writing to the DOM forces the browser to recalculate layouts multiple times
- Inefficient updates: Updating more elements than necessary
- Excessive repaints: Causing the browser to redraw parts of the screen unnecessarily
Before React, developers used libraries like jQuery to make DOM manipulation easier, but these tools didn’t solve the underlying performance issues.
React’s Solution: The Virtual DOM
React addresses these performance challenges with its Virtual DOM implementation.
What is Virtual DOM?
The Virtual DOM is a lightweight JavaScript representation of the real DOM. It’s essentially a tree of JavaScript objects that mimics the structure of the DOM.
// Example of a Virtual DOM representation
const virtualNode = {
type: 'div',
props: {
id: 'container',
children: [
{
type: 'h1',
props: {
children: 'Hello World'
}
},
{
type: 'p',
props: {
children: 'This is a paragraph.'
}
}
]
}
};
How the Virtual DOM Works
React’s Virtual DOM process follows these steps:
- Initial render: React creates a virtual DOM tree from your components
- Conversion: React converts the virtual DOM to real DOM elements
- When state changes: React creates a new virtual DOM tree
- Diffing: React compares the new virtual DOM with the previous one using a diffing algorithm
- Reconciliation: React identifies the minimal set of changes needed to update the real DOM
- Batched updates: React applies all the necessary changes to the real DOM in a single batch
Show Image
React Fiber: The Reconciliation Engine
In React 16, the team introduced a new reconciliation engine called React Fiber.
What is React Fiber?
React Fiber is a complete rewrite of React’s core algorithm. Its main features include:
- Incremental rendering: Breaking rendering work into chunks that can be paused, resumed, and prioritized
- Error boundaries: Better error handling in components
- Improved support for animation, layout, and gestures
- Foundation for async rendering
How Fiber Works
Fiber works by creating a linked list of nodes (fibers) that can be processed incrementally. This allows React to:
- Pause work and come back to it later
- Assign priority to different types of updates
- Reuse previously completed work
- Abort work if it’s no longer needed
ReactDOM: The Bridge to the Browser
The react-dom package serves as the bridge between React’s Virtual DOM and the browser’s real DOM.
Key ReactDOM Functions
ReactDOM.render()
This is the main function for rendering React elements to the DOM:
import React from 'react';
import ReactDOM from 'react-dom';
const element = <h1>Hello, world!</h1>;
ReactDOM.render(element, document.getElementById('root'));
In React 18+, this has been replaced with ReactDOM.createRoot():
import React from 'react';
import { createRoot } from 'react-dom/client';
const element = <h1>Hello, world!</h1>;
const root = createRoot(document.getElementById('root'));
root.render(element);
ReactDOM.hydrate()
Used for server-side rendering to “hydrate” a container whose HTML contents were rendered by ReactDOMServer:
import React from 'react';
import ReactDOM from 'react-dom';
const element = <h1>Hello, world!</h1>;
ReactDOM.hydrate(element, document.getElementById('root'));
In React 18+, use hydrateRoot():
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
const element = <h1>Hello, world!</h1>;
hydrateRoot(document.getElementById('root'), element);
ReactDOM.findDOMNode() (Legacy)
Returns the browser DOM element for a component instance:
import React from 'react';
import ReactDOM from 'react-dom';
class MyComponent extends React.Component {
 componentDidMount() {
  const domNode = ReactDOM.findDOMNode(this);
  // Do something with the DOM node
 }
Â
 render() {
  return <div>My Component</div>;
 }
}
Note: findDOMNode() is deprecated in strict mode. Instead, use refs.
ReactDOM.createPortal()
Renders children into a DOM node that exists outside the DOM hierarchy of the parent component:
import React from 'react';
import ReactDOM from 'react-dom';
function Modal({ children }) {
return ReactDOM.createPortal(
children,
document.getElementById('modal-root')
);
}
function App() {
return (
<div>
<h1>App Content</h1>
<Modal>
<h2>Modal Content</h2>
<p>This appears in the modal container, not in the App div.</p>
</Modal>
</div>
);
}
The Reconciliation Process in Detail
1. Component Tree to Virtual DOM
When you write a React component:
function App() {
 return (
  <div className="app">
   <Header title="My App" />
   <Content />
  </div>
 );
}
React creates a Virtual DOM representation:
{
 type: 'div',
 props: {
  className: 'app',
  children: [
   {
    type: Header,
    props: { title: 'My App' }
   },
   {
    type: Content,
    props: {}
   }
  ]
 }
}
2. Diffing Algorithm
When state or props change, React creates a new Virtual DOM tree and compares it with the previous one using a diffing algorithm.
Key Assumptions in React’s Diffing Algorithm
- Different component types produce different trees: If a div changes to a span, React rebuilds the entire subtree rather than trying to match children.
- Elements with stable keys maintain identity across renders: The key prop helps React identify which items have changed, been added, or been removed.
Diffing Process
- Root Elements: If the root elements are different types, React tears down the old tree and builds the new one from scratch.
- Same Type DOM Elements: React looks at the attributes of both and only updates the changed attributes.
// Before
<div className="before" title="tooltip">Content</div>
// After
<div className="after" title="tooltip">Content</div>// React only updates the className
- Same Type Component Elements: The component instance stays the same, only the props are updated, and componentDidUpdate() or appropriate hooks are called.
- Recursion on Children: By default, React recursively compares children in order.
// Before
<ul>
<li>first</li>
<li>second</li>
</ul>
// After
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
// React adds the third list item
- Keys: When children have keys, React uses the key to match children in the original tree with children in the subsequent tree.
// Before
<ul>
<li key="1">first</li>
<li key="2">second</li>
</ul>
// After
<ul>
<li key="2">second</li>
<li key="1">first</li>
</ul>
// React moves the elements instead of recreating them
3. Batch Updates
React batches state updates and DOM manipulations to minimize browser reflows and repaints. This is especially important for complex UI updates.
Keys in React: The Special Prop
As shown in the diffing algorithm, keys play a crucial role in efficient list rendering.
The Importance of Keys
Keys help React identify which items have changed, been added, or been removed in a list. They should be stable, predictable, and unique among siblings.
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Common Key Mistakes
Using Index as Key (Usually Bad)
// Not recommended unless the list is static and never reordered
{items.map((item, index) => (
 <ListItem key={index} item={item} />
))}
Using the array index as a key can cause issues if the list order changes or items are added/removed in the middle of the list.
Not Using Keys at All
// Missing keys warning
{items.map(item => (
 <ListItem item={item} />
))}
React will use indices by default and issue a warning.
When to Use Index as Key
Sometimes using the index as a key is acceptable:
- The list is static and will not change
- The items in the list have no stable IDs
- The list will never be reordered or filtered
Measuring Performance with React DevTools
React DevTools provides tools for measuring the performance impact of your components and the Virtual DOM reconciliation process.
Profiler Tab
React DevTools includes a Profiler tab that lets you record and analyze component rendering performance.
- Open React DevTools in your browser
- Switch to the Profiler tab
- Click the Record button
- Interact with your app
- Stop recording and analyze the results
The Profiler shows:
- Which components rendered and why
- How long each component took to render
- The “commit” timeline for rendering updates
Using React.memo for Optimization
React.memo is a higher-order component that memoizes your component, preventing unnecessary re-renders if props haven’t changed:
const MemoizedComponent = React.memo(function MyComponent(props) {
 // Your component code
});
You can also provide a custom comparison function:
const MemoizedComponent = React.memo(
 function MyComponent(props) {
  // Your component code
 },
 (prevProps, nextProps) => {
  // Return true if passing nextProps to render would return
  // the same result as passing prevProps to render
  return prevProps.value === nextProps.value;
 }
);
React.StrictMode and DOM Operations
React.StrictMode is a tool for highlighting potential problems in your application during development:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
StrictMode:
- Identifies unsafe lifecycle methods
- Warns about legacy string ref API usage
- Warns about deprecated findDOMNode usage
- Detects unexpected side effects
- Ensures reusable state
It helps you spot potential issues related to DOM operations and the rendering process.
Practical Example: List with Optimized Rendering
Let’s build a todo list that demonstrates efficient DOM updates using React’s Virtual DOM and keys:
import React, { useState } from 'react';
// Individual Todo item component
const TodoItem = React.memo(function TodoItem({ todo, onToggle, onDelete }) {
console.log(`Rendering TodoItem: ${todo.id}`);
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span className="todo-text">{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
// Todo List Component
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: true },
{ id: 2, text: 'Build a project', completed: false },
{ id: 3, text: 'Deploy to production', completed: false }
]);
const [newTodoText, setNewTodoText] = useState('');
const [nextId, setNextId] = useState(4);
const handleAddTodo = () => {
if (!newTodoText.trim()) return;
setTodos([
...todos,
{
id: nextId,
text: newTodoText,
completed: false
}
]);
setNextId(nextId + 1);
setNewTodoText('');
};
const handleToggleTodo = (id) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const handleDeleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div className="todo-app">
<h1>Todo List</h1>
<div className="add-todo">
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="Add a new todo"
/>
<button onClick={handleAddTodo}>Add</button>
</div>
<ul className="todo-list">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggleTodo}
onDelete={handleDeleteTodo}
/>
))}
</ul>
<div className="todo-stats">
<p>{todos.filter(todo => todo.completed).length} completed / {todos.length} total</p>
</div>
</div>
);
}
export default TodoList;
Key Optimization Points in the Example
- Proper Key Usage: Each TodoItem has a stable, unique id as its key
- React.memo: The TodoItem component uses React.memo to prevent unnecessary re-renders
- State Updates: State updates are done immutably, which helps React identify changes
- Component Composition: By breaking the UI into components, React can update only what’s necessary
Summary
In this chapter, we’ve covered:
- The Document Object Model (DOM) and how browsers render web pages
- The performance challenges of direct DOM manipulation
- React’s Virtual DOM as a solution to these challenges
- The reconciliation process and diffing algorithm
- React Fiber architecture
- Key ReactDOM functions for working with the browser
- The importance of keys in efficient list rendering
- Performance measurement tools
- Optimization techniques like React.memo
- A practical example of optimized list rendering
Understanding the Virtual DOM is crucial for optimizing React applications. By leveraging React’s efficient rendering strategy and following best practices, you can build performant applications even with complex UIs.