React reconciliation is the engine behind your UI updates. Learn how DOM diffing works and how to minimize mutations to keep your React app fast.
Last month, I spent about three days debugging a sluggish dashboard. Every time a user typed in a search input, the entire list of 500 items re-rendered, causing a noticeable delay of roughly 120ms per keystroke. The culprit wasn't the logic itself, but a misunderstanding of how React handles updates.
To write performant code, you have to stop thinking about "updating the DOM" and start thinking about how React manages its internal tree.
At its core, React reconciliation is the process where React updates the DOM to match the latest version of your component tree. When you call setState or pass new props, React doesn't just blast the entire DOM with new HTML. That would be incredibly expensive.
Instead, it creates a new "Virtual DOM" tree and compares it to the previous one. This is the DOM diffing phase. React uses a heuristic algorithm that operates in $O(n)$ time. It assumes two things:
key prop.If the root element type changes—say, from a <div> to a <span>—React throws out the old tree and builds a new one from scratch. If the type stays the same, it only updates the attributes that changed. This is where most performance gains happen.
I once worked with a junior dev who used array indices as key values in a dynamic list. It seemed fine until we started deleting items from the middle of the list. Because the indices shifted, React got confused about which component instance belonged to which data.
We saw the state of the third item jump to the second item, and the browser repainted the entire list. It was a mess.
If you want to master this, you should look into how React reconciliation and component state persistence: A mental model dictates how your UI holds onto data. When you use stable IDs—like a database primary key—instead of indices, you allow React to move elements around rather than destroying and recreating them.
You can't optimize what you don't track. To minimize mutations, you need to be surgical about where state lives. If a parent component re-renders, all its children re-render by default. This is often harmless, but when you have deep trees, it adds up.
Here is a quick pattern I use to keep things lean:
React.memo for expensive components that don't need to re-render unless their props actually change.If you're dealing with complex UI, understanding React rendering and layout shifts: A guide to stable UIs will help you prevent the browser from doing extra work during those paint cycles.
People often obsess over micro-optimizations, but usually, the bottleneck is just unnecessary re-renders. Before you reach for useMemo or useCallback, check your component structure.
Are you passing an object literal as a prop?
JAVASCRIPT// Bad: creates a new object on every render <ExpensiveComponent config={{ theme: CE9178">'dark' }} /> // Good: memoize the object outside the component or use useMemo const config = useMemo(() => ({ theme: CE9178">'dark' }), []); <ExpensiveComponent config={config} />
When you pass an object literal, the reference changes every time the parent renders. Even if the content is identical, React thinks the props have changed and triggers a re-render. This breaks the shallow comparison that React.memo relies on.
Does Virtual DOM mean the DOM isn't updated? No. The Virtual DOM is just a lightweight JavaScript object representation. React still updates the real DOM, but it does so only after calculating the absolute minimum set of changes required.
Is reconciliation the same as rendering? Not exactly. Rendering is the process of calling your components to get the new UI tree. Reconciliation is the process of comparing that new tree to the old one to decide what to change in the actual DOM.
Should I use key={Math.random()}?
Never. This forces React to destroy and recreate the component on every single render. It kills performance and wipes out any local state, like input values or scroll positions.
I’m still experimenting with how to better profile these re-renders in production environments. Using the React DevTools Profiler is great for local testing, but catching these issues in the wild is harder.
Next time you're building a feature, try to visualize the tree. Ask yourself: "If this state updates, which nodes are actually forced to re-evaluate?" If you can answer that, you’re already ahead of most engineers. Don't worry about getting it perfect on the first pass—just aim to keep your component tree as shallow and stable as possible.
React performance depends on knowing when to memoize. Learn the mental model for using useMemo and useCallback effectively without falling into optimization traps.