"Why did this component re-render?" is the React question everyone asks and almost no one can answer at a glance — because the answer is invisible. So I built a tool that makes it visible.
▶ Live demo: https://react-render-visualizer.vercel.app/
Source (React 19 + TS + Vite): https://github.com/dev48v/react-render-visualizer
The component tree on screen is a real React tree. Every node flashes when it actually renders, counts its renders, and labels why it rendered. A side panel keeps a running wasted renders counter.
The three rules, made visible
There are really only three reasons a component re-renders, and the tool shows each one:
-
A parent re-render re-renders all of its children — by default. With
React.memooff, click thetick++button and the entire tree flashes, even components whose props never changed. The wasted counter climbs. -
React.memoskips the re-render when props are unchanged. Turn memo on, clicktick++→ onlyContent → Counterre-render (they're the ones that actually usetick);HeaderandSidebarskip. Wasted renders: 0. -
A fresh object/array/function prop defeats
memo. Turn "stable props" off so the parent hands down a brand-newstyleobject every render, and click again →SidebarandContentre-render despite being memoized, because memo's shallow compare sees a changed reference. That's theuseMemo/useCallbacklesson, concrete.
| memo | stable props |
tick++ re-renders |
|---|---|---|
| off | — | everything |
| on | on | only Content → Counter
|
| on | off |
Sidebar + Content too (new style ref) |
How it's built
The whole thing hinges on a small hook:
function useTracker(name: string, watch: Record<string, unknown>) {
const n = useRef(0);
const prev = useRef(watch);
n.current++;
let reason: string;
if (n.current === 1) reason = "mount";
else {
const changed = Object.keys(watch).filter(k => watch[k] !== prev.current[k]);
reason = changed.length
? "props changed: " + changed.join(", ")
: "parent re-rendered (no prop change)";
}
prev.current = watch;
useEffect(() => { record(name, reason); /* + flash the node */ });
return reason;
}
It diffs this render's watched props against the previous render to classify the cause. One subtlety worth stealing: the render events go into a tiny external store read with useSyncExternalStore, not React state. If the panels used useState, recording a render would itself trigger renders and pollute the very counts you're measuring. An external store updates the panels without re-rendering the tree under test.
No StrictMode, on purpose — StrictMode intentionally double-invokes render in dev, which would double every count.
Why a playground beats an article (even this one)
You can read "memo does a shallow prop comparison" ten times and still ship a style={{...}} that silently breaks it. Clicking a button and watching the wasted counter jump from 0 to 12 the instant you pass an inline object teaches it in one go.
One npm install, zero UI dependencies. If it made re-renders click for you, a star helps others find it: https://github.com/dev48v/react-render-visualizer
Top comments (0)