Spot unnecessary re-renders visually, without recording a profiler session
Before you open the Profiler tab and start a recording, unnecessary re-renders can often be made visible with a single click. The React DevTools option Highlight updates when components render colors every component the moment its render function runs, live in the browser as you click, type, and navigate.
Table of Contents
- 1. Why re-renders stay invisible in everyday development
- 2. Enabling the option in the React DevTools
- 3. Reading the colored borders and flashes correctly
- 4. A concrete example of an unnecessary re-render
- 5. Common causes of unnecessary re-renders
- 6. Fixing the root cause with React.memo, useCallback, and useMemo
- 7. When the Profiler tab is the better choice
- 8. Building component highlighting into the daily workflow
- 9. Limits and common pitfalls of the method
- 10. Summary
- 11. FAQ
1. Why re-renders stay invisible in everyday development
React re-renders components very frequently for good reason: every state change, every new prop, and every context switch can trigger a render function to run again. In most cases that is completely unproblematic, because React diffs the virtual DOM and only updates the parts of the real DOM that actually changed. The problem is not the re-render itself, it is that it stays invisible in the ordinary browser window. A user only sees that the page responds, not how often or how many components were unnecessarily recomputed along the way.
That invisibility is exactly what makes performance problems in React applications so hard to pin down. A form with thirty input fields can quietly re-render an entire sidebar, header, and an expensive chart component on every keystroke without anything visibly changing. The React DevTools close exactly that gap: instead of reconstructing render cycles from logs or profiler charts, they make them visible as colored flashes right in the running browser window, in real time and without a single line of additional code.
2. Enabling the option in the React DevTools
Enabling it takes a few seconds. Open the browser DevTools, switch to the Components tab, which the React DevTools extension for Chrome, Firefox, or Edge adds automatically, and click the gear icon for settings. In the General tab there is a checkbox labeled Highlight updates when components render. Once it is active, React DevTools draws a briefly flashing colored border around the affected component in the tree on every commit, meaning every render pass that was actually executed.
No build step, no import, and no instrumentation in your own code is required, the option lives entirely inside the browser extension. It works identically in the standalone React DevTools app used for React Native or Electron contexts. The feature is relevant exclusively during development: in a production build without the extension installed, end users see nothing of it, and the option has no effect whatsoever on bundle size or runtime performance.
3. Reading the colored borders and flashes correctly
Every re-render of a component produces a brief, colored flash around its rendered area in the browser. The color roughly encodes render frequency relative to other commits: a bluish flash indicates infrequent updates, a green tone points to more frequent ones, while more intense yellow and red tones signal that a component is re-rendering unusually often in a short span. This color scale is not an exact measurement tool, it is a quick visual heuristic meant to draw the eye to outliers in the component tree immediately.
One important nuance: the highlighting indicates that a component's render function ran, not necessarily that anything visibly changed on screen. React can render a component and produce the exact same virtual DOM as before, so nothing happens to the real DOM even though the render function ran to completion and cost CPU time. Anyone watching only for visible changes misses exactly these cases, where unnecessary work happens without anything moving on screen at all.
4. A concrete example of an unnecessary re-render
Picture a page with a counter button in the header and an ExpensiveList below it that renders a larger product list and needs no data from the counter at all. Both components share the same parent, which keeps the counter value in a useState. Without further measures, every click on the button causes the parent to re-render and, by default, also calls every child component again, even the ones that do not depend on the changed state at all.
With highlighting enabled the result is immediately visible: clicking the counter button flashes not only the number in the header but also the entire border of the ExpensiveList, even though its content does not change at all. This exact pattern, an isolated state change triggering a cascading re-render wave across entirely unrelated subtrees, is the most common category of unnecessary re-renders in React applications and is otherwise nearly impossible to spot with the naked eye.
function Dashboard() {
const [count, setCount] = useState(0);
return (
<div>
<Header count={count} onIncrement={() => setCount((c) => c + 1)} />
{/* ExpensiveList does not depend on "count" but re-renders anyway */}
<ExpensiveList items={PRODUCTS} />
</div>
);
}
function ExpensiveList({ items }) {
console.log("ExpensiveList renders");
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
5. Common causes of unnecessary re-renders
The most frequent cause is structural and has little to do with the specific example: in React, a parent component re-renders all child components by default whenever its own state changes, regardless of whether those children even consume the affected props. This gets worse with inline object, array, and function props, for example a handler like onClick={() => doSomething()} written directly in JSX. On every render a new function reference is created, even if its content stays identical, which defeats any downstream identity comparison.
A second common source is React Context: if the value passed to a provider is created as a new object literal on every render of the parent, for example value={{ user, theme }} written directly in JSX, then every consumer of that context re-renders on every change, even if only one of the two fields actually changed. Global contexts for theme, auth, or language in particular then potentially affect the entire application and produce exactly the kind of cascading highlighting that jumps out immediately in the component tree.
// Problematic: a new object on every render
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
// { user, theme } is a new object reference on every render
return (
<AppContext.Provider value={{ user, theme }}>
{children}
</AppContext.Provider>
);
}
6. Fixing the root cause with React.memo, useCallback, and useMemo
The fix addresses two spots at once, because they depend on each other: the child component gets wrapped in React.memo so it skips a re-render as long as its props are shallowly unchanged. That alone is not enough if the parent keeps generating new prop references on every pass anyway. That is why callback functions need to be stabilized with useCallback and object or array literals with useMemo, each with a dependency array that only produces a new reference on a genuine content change.
After the fix, the very same highlighting technique shows the result: clicking the counter button now flashes only the header, the ExpensiveList stays dark because React.memo recognizes the comparison of the now-stable props and skips the render call. It matters not to reflexively slap React.memo on every component: the comparison itself costs time too, and for small, cheap-to-render components memoization can create more overhead than it saves. Highlighting also helps here, comparing the actual benefit objectively before and after the change.
const ExpensiveList = memo(function ExpensiveList({ items }) {
console.log("ExpensiveList renders");
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
function Dashboard() {
const [count, setCount] = useState(0);
// useCallback keeps the function reference stable
const handleIncrement = useCallback(() => setCount((c) => c + 1), []);
// items already arrives as a stable reference from outside (e.g. a constant)
return (
<div>
<Header count={count} onIncrement={handleIncrement} />
<ExpensiveList items={PRODUCTS} />
</div>
);
}
7. When the Profiler tab is the better choice
Component highlighting reliably answers whether a component re-renders at all, but not how expensive that render actually was. That is exactly what the Profiler tab is for: a recording produces a flame chart with render duration per component in milliseconds, the number of commits during the recording, and, with the right option enabled, even hints about which props or which state change triggered a given render. That makes it the right tool once the question moves from a rough first impression to solid numbers.
In practice a clear sequence pays off: highlighting first, to find suspicious candidates in the tree at all, then the profiler, to quantify whether optimizing them is actually worthwhile. A component that visibly flashes but whose render, according to the profiler, only takes 0.1 milliseconds is rarely the cause of noticeable jank. Wrapping every flashing component in React.memo right away often costs more time than the actual performance gain it delivers.
8. Building component highlighting into the daily workflow
The option delivers the most value once it becomes a fixed part of the development routine instead of only being switched on when performance complaints arrive. A short check right after adding new state, a new context, or a newly wired prop chain is particularly worthwhile: a glance at the component tree during a typical interaction immediately shows whether the change ripples out unexpectedly far. Some teams turn this into a fixed step in self-review before every pull request that touches UI-relevant code.
For deeper cases, a small custom debug hook that logs render counts and changed props straight to the console is a useful addition whenever the visual flashing alone is not enough to isolate the cause. For automated, ongoing monitoring beyond manual observation, libraries like why-did-you-render or React Scan are worth adding, since they name the cause directly instead of only visualizing it. Component highlighting remains the fastest first step regardless, since it is available instantly with no extra dependency at all.
function useRenderCount(label) {
const countRef = useRef(0);
countRef.current += 1;
useEffect(() => {
console.log(`${label} rendered ${countRef.current} time(s)`);
});
return countRef.current;
}
function ExpensiveList({ items }) {
useRenderCount("ExpensiveList");
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
9. Limits and common pitfalls of the method
In Strict Mode, React 18 and 19 deliberately run every render and selected effects twice in the development build, in order to surface hidden side effects early. That causes component highlighting to flash roughly twice as often under Strict Mode as it would in production behavior, which developers new to Strict Mode easily mistake for a real performance problem. A quick look at production build behavior, or consciously ignoring the first duplication, clears this up before chasing a phantom cause.
The second limit is more subtle: no flashing is not proof of good performance, because expensive computations can also live outside the actual render cycle, for example inside a heavy useEffect, an expensive event handler, or a synchronous calculation that runs independently of the component tree. Component highlighting only answers the question of render frequency, not the total cost of an interaction. Relying on it exclusively misses entire classes of performance problems that only surface in the profiler or in real performance traces.
| Method | Shows | Effort | Best time to use it |
|---|---|---|---|
| Component highlighting | That a component renders, colored, live in the browser | One click, no recording needed | First quick scan during development |
| Profiler tab | Render duration, commit count, trigger per commit | Start and stop a recording | Solid numbers before a targeted optimization |
| why-did-you-render | Exact cause: which prop or state value changed | Add and configure a library | Deep analysis of stubborn individual cases |
| Custom useRenderCount hook | Render count and timing in the console | A few lines of your own code | Focused debugging of one specific component |
Mironsoft
React architecture, performance, and Magento frontend integration
React frontends that stay fast instead of slowing down with every feature?
We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.
Performance Audit
Systematically measuring and fixing re-renders, bundle size, and load times.
State Architecture
Cleanly separating context, client state, and server state instead of mixing everything.
Magento Integration
Building robust, type-safe GraphQL or REST integration with Magento.
10. Summary
React DevTools Highlighting: The Essentials at a Glance
Enabling it
Open the Components tab in React DevTools, click the gear, check Highlight updates when components render.
Color code
Blue to red indicates relative render frequency, a quick heuristic rather than an exact measurement.
Most common cause
Inline object, array, and function props, plus context values that are not kept stable.
The fix
React.memo on the child component, combined with useCallback and useMemo at the source of the props.