React is known for being fast. Today, we will explore "What makes it fast?" and "How can we make it even faster?"
What Triggers a Render?
In React, a component re-renders when,
- The state of any component has changed (using useState or useReducer).
- The component receives new props.
- A parent component re-renders, causing its child components to re-render.
Reconciliation: How React Knows What to Change
In React, Reconciliation is the process used to determine how efficiently update the DOM when any change occurs in the UI.
Firstly, React calls the render() method of a component to create a new Virtual DOM, and then compares it with the previous Virtual DOM. After calculating the minimum number of changes needed, it updates only the changed parts of the real DOM. This complete process comes under the Reconciliation.
How does it compare to the Elements?
- It makes use of some smart assumptions, allowing us to reconcile faster.
- It assumes that elements of different types produce different trees and need full replacement (<div> vs <p>).
- To identify elements in the lists, it uses keys.
- It always avoids deep comparisons. (React is optimized for performance, not perfection.)
Example: This example shows list rendering.
const users = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
If your list is not static, never use an index, as it can break reconciliation. Always use stable keys (such as IDs).
Batching: Grouping Updates Into One Render
The process of combining or grouping multiple state updates into a single re-render is known as Batching. It improves the application's performance by reducing the frequency of DOM updates.
Before React 18, state updates and async calls were not batched; only event handlers were triggered, and updates were batched. However, with React 18 and automatic batching, State updates are now batched in all contexts, including those involving promises and async/await.
function App() {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
function updateBoth() {
setX((prev) => prev + 1);
setY((prev) => prev + 1);
}
return <button onClick={updateBoth}>Click</button>;
}
The above code triggers only one render, even though two states changed.
Memoization: Preventing Wasted Renders
It is about caching results of computations or render logic so that React doesn't re-do the same work over and over again.
React.memo
The react. Memo is used to prevent a component from re-rendering if its props haven't changed.
const Profile = React.memo(function Profile({ name }) {
console.log("Rendering Profile...");
return <p>{name}</p>;
});
In the above code, even if the parent re-renders, Profile won't re-render unless the name prop changes.
useMemo
It caches the result of an expensive calculation (such as sorting, filtering, or heavy computation) to avoid repeating it unnecessarily, as doing so every time can slow things down.
import React, { useMemo, useState } from 'react';
function ItemList({ items }) {
const [query, setQuery] = useState("");
const sortedItems = useMemo(() => {
console.log("Sorting items...");
return [...items].sort((a, b) => a.value - b.value);
}, [items]);
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
/>
<ul>
{sortedItems
.filter(item => item.name.includes(query))
.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</>
);
}
Without useMemo, Sorting happens on every keystroke in the input. Even if items didn't change, it still sorts again. However, with useMemo, sorting only occurs when items change, resulting in faster renders while typing into the input.
useCallback
It caches the function reference between renders, because in JavaScript, functions are re-created every time a component renders.
Suppose you pass a function as a prop to React.Memoized child, the child still re-renders because the function is technically a new object each time, even if the logic hasn’t changed. useCallback() keeps the function reference the same, unless its dependencies change.
Example: Callback Optimization with Child Component.
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click Me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback, new function every render
// With useCallback, same function reference
const handleClick = useCallback(() => {
console.log("Clicked");
}, []);
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Child onClick={handleClick} />
</>
);
}
- Without useCallback, every time the Parent renders, handleClick is a new function. Even if the Child is wrapped in React.Memo, it still renders.
- With useCallback, handleClick only changes if dependencies change. Child skips unnecessary re-renders = performance win.
Conclusion
React is designed to render efficiently by default. But as your app scales, you need to help React help you.
- Know when and why components re-render.
- Use memo, useMemo, and useCallback wisely.
- Understand how batching and reconciliation impact performance.
With these tools, you’ll be able to build fast, fluid, and responsive apps, even as complexity grows.