On this page
Introduction
Before optimizing a React application, we must understand how React updates the interface and how to measure an application’s performance. Optimization means reducing load time, minimizing memory use and keeping interactions responsive as an application grows in complexity and traffic. Done well, it improves user engagement, can reduce hosting costs, supports search ranking — page speed is a ranking factor — and often leaves code more modular and easier to maintain.
Done badly, it adds complexity in exactly the wrong places. The thesis of this paper is simple: measure first.
How React updates the interface
When state changes, React re-renders the affected components to produce a new element tree, compares it with the previous one, and applies only the differences to the DOM. Work therefore happens in two phases: a render phase, where components run and the new tree is computed, and a commit phase, where changes are written to the DOM.
Diagram: a state change triggers the render phase, where components re-run and the new tree is diffed against the previous one; the commit phase then applies only the changed nodes to the DOM.
Common classes of bottleneck
- Unnecessary re-renders
- A state change high in the tree re-renders everything below it, including components whose output does not change.
- Large component trees
- Deeply nested trees are slow to render and update, and amplify every unnecessary re-render.
- Inefficient data fetching
- Waterfalls and over-fetching delay meaningful content and inflate load times.
- Bloated bundles
- Large JavaScript bundles take longer to download, parse and execute, delaying interactivity.
- Memory leaks
- Subscriptions and timers that outlive their components degrade performance until the app slows or crashes.
- Improper state use
- Setting state in loops or storing derived data multiplies renders without adding information.
- Unoptimized images
- Oversized images dominate transfer size and memory on many pages.
Method: profiling
Profiling measures where time is actually spent. The React DevTools browser extension includes a Profiler tab; Chrome DevTools’ Performance panel and Lighthouse complement it for load and runtime analysis.
Capturing a profile
- Open the React DevTools Profiler tab and press Record.
- Use the application as a person would — the profiler records every commit.
- Press Record again to stop and inspect the session.
The flame graph shows each commit as a tree whose bar widths represent render time; the ranked view sorts components by cost. Start with the widest bars and the components that render most often.
Measuring in code
For repeatable measurements — or to send timings to monitoring — wrap a subtree in the Profiler component. Its onRender callback fires after every commit of that subtree:
import { Profiler } from "react";
function onRender(id, phase, actualDuration, baseDuration) {
// phase: "mount" | "update" | "nested-update"
console.log(\`\${id} \${phase}: \${actualDuration.toFixed(1)}ms (base \${baseDuration.toFixed(1)}ms)\`);
}
export default function App() {
return (
<Profiler id="Dashboard" onRender={onRender}>
<Dashboard />
</Profiler>
);
}
actualDuration is the time spent rendering this commit; baseDuration estimates the cost of rendering the whole subtree without memoization. A large gap between them shows memoization is working; a small one shows it is not helping.
From data to change
- Collect a profile of a realistic interaction.
- Analyze it for the components that take the most time or render most often.
- Prioritize the changes likely to have the largest effect.
- Optimize narrowly — one bottleneck at a time.
- Verify with a new profile, comparing against the original.
- Iterate until the interaction meets its budget.
Keep reading — it’s free
The remaining 4 min of this story is for Dispatch subscribers. Subscribing takes one field, and the whole story opens right here.
Subscribe to read the rest
Free. One email when there’s something worth reading.
Originally published at Aviox Blogs engineering handbook.

