npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-render-detective

v0.7.0

Published

Know WHY your React components render — not just that they did.

Readme

React Render Detective

npm bundle deps license

Know why your React components render.

📖 Website · Guide · API · Feasibility report · Benchmarks

Status: 0.7.0, early release. 76 tests pass on React 18 and 19; benchmarks and bundle budgets are green; the packed package is verified in a clean install for ESM, CJS and TypeScript consumers; and the demo dashboard has been driven end to end in Chrome, which found four real defects the jsdom suite had missed (see the changelog). Not yet exercised: Suspense and error-boundary edges, and any app that isn't the demo. Treat it as a preview — issues welcome.

Not just:

UserProfile rendered 47 times.

But:

UserProfile rendered because `user` changed by reference.
Its values are identical. Dashboard recreated the object.

Debug React rendering without scattering console.log through your components.


Install

npm install react-render-detective
import { init } from "react-render-detective";

if (process.env.NODE_ENV !== "production") {
  init();
}

Then wrap the components you're investigating:

import { withRenderDetective } from "react-render-detective";

const UserProfile = withRenderDetective(function UserProfile({ user, onSave }) {
  return /* … */;
});

That's it. No server, no account, no API key, no browser extension, no data leaves your machine.

▲ [RRD] UserProfile #47  Reason: prop changed (reference only)  Changed: user  Duration: 8.4ms

Ask for the whole story at any time:

import { explain } from "react-render-detective";
console.log(explain("UserProfile"));
UserProfile

47 recorded renders

Why?
  78% of renders followed `user` changing by reference while its contents stayed the same.

Breakdown
  props                  37  79%
  parent                  8  17%
  mount                   1   2%
  state-or-external       1   2%

Reference-only prop changes
  user                   37  79%  (object)
  onSave                 31  66%  (function)

Cost
  average       8.4ms
  total         394.8ms
  potentially avoidable  37 render(s), ~310.8ms

Next step
  Find where `user` is created in Dashboard and stabilise it (useMemo, or pass the
  primitive fields you use).

Confidence: high

What makes this different

It answers causality, not counts:

| Question | Answer | | --- | --- | | What rendered? | component, render number, mount vs update | | Why? | props · parent · context · state · which store selector — with the evidence | | What changed? | per prop: value change vs reference-only change | | Where from? | the nearest instrumented ancestor, and whether it re-rendered | | How expensive? | subtree duration, and self duration with descendants subtracted | | How sure are we? | every diagnosis carries high / medium / low | | What next? | an evidence-based suggestion, or nothing |

And it refuses to guess. When the runtime cannot tell you why something rendered, it says:

Cause could not be determined reliably.

Three rules it holds to, which most render-debugging advice does not:

  1. Rendering is not a bug. Output says render, potentially avoidable render, slow render — never "BAD RENDER".
  2. Memoization is a trade. React.memo / useMemo / useCallback are suggested only when the evidence supports them, with the cost shown so you can decide.
  3. StrictMode is not a 2× regression. Double-invoked renders are labelled as development replays and excluded from every statistic.

Automatic instrumentation

Wrapping by hand only finds problems you already suspected. One line instruments the whole app and adds source locations:

// vite.config.ts
import { renderDetective } from "react-render-detective/vite";
export default defineConfig({ plugins: [renderDetective(), react()] });
TableRow   src/App.tsx:85:7

Why?
  100% of updates followed `onSelect` changing by reference while its contents stayed the same.

Next step
  Trace `onSelect` back from ProductTable (src/App.tsx:110:1), which passes it to TableRow,
  and stabilise it where it is created (useCallback, or hoist it out of the component).

Babel is supported too, for Next.js, webpack and Remix — see the guide. Dev-only; it removes itself from production builds.

Integration modes

withRenderDetective — most accurate

const UserProfile = withRenderDetective(UserProfileImpl, { name: "UserProfile" });

Full diagnosis: per-prop diffing, parent attribution, Profiler timings.

<RenderDetective> — zero refactor

<RenderDetective name="UserProfile">
  <UserProfile />
</RenderDetective>

Timings and parent propagation, but it only sees the children element — it cannot attribute a render to an individual prop.

useRenderDiagnostics — from inside

function UserProfile(props) {
  const diagnostics = useRenderDiagnostics("UserProfile", props);
  // …
}

Catches state-driven renders too, but a hook cannot install a <Profiler> around its own component, so no durations are available in this mode.

Naming what the runtime can't see

const [items, setItems] = useTrackedState("items", []);       // proves a state-driven render
useTrackedContextValue("AuthContext", value);                  // inside your provider
useTrackedEffect("sync", () => { … }, [userId]);               // which declared dep changed

Overlay

if (process.env.NODE_ENV !== "production") {
  const { mountOverlay } = await import("react-render-detective/overlay");
  mountOverlay();
}

A floating panel with live totals, the most expensive components, and the full explain() output for whichever one you select. It renders in a shadow DOM outside your React tree — an inspector that re-rendered the tree it measures would be measuring itself.

The overlay is optional and lazily imported; the core is fully usable from the console.


Configuration

init({
  enabled: process.env.NODE_ENV !== "production",
  mode: "console",              // "silent" | "console" | "verbose"
  include: [/^Dashboard/],      // empty = everything
  exclude: ["Icon", "Button"],  // exclude wins over include
  samplingRate: 1,              // 0–1, decided once per component instance
  maxEvents: 1000,              // bounded ring buffer
  slowRenderThreshold: 16,
  thresholds: { monitor: 5, slow: 16, verySlow: 50, critical: 100 },
  inspection: { depth: 1, maxObjectKeys: 20, maxArrayLength: 20, maxStringLength: 120 },
  compareFunctionSource: false, // spot recreated inline closures (opt-in)
  onEvent: (event) => {},
});

init() is idempotent — Fast Refresh, duplicate module copies and repeated calls reconfigure the single instance instead of stacking three copies of the debugger.

Full API: docs/API.md.


Safety

  • Nothing leaves your machine. No network calls, no telemetry, no analytics, no storage.
  • Off until you ask. Importing the package registers nothing. init() is the switch, and it turns diagnostics on unless it can positively see NODE_ENV === "production" — guard the call so your bundler removes it entirely. With enabled: false nothing is registered and no <Profiler> is mounted.
  • Fail-safe. Every instrumentation path is wrapped: a throwing getter, an exploding subscriber or an un-inspectable prop degrades the diagnostic, never your app.
  • Bounded. Ring-buffered events, capped inspection depth/width, props released on unmount.

Cost

Measured against the built package (npm run bench, full numbers in docs/BENCHMARKS.md):

| | | | --- | --- | | Per instrumented component | ~7µs structural + ~7–16µs recording, well inside the 0.1ms target | | Bundle, everything loaded | 12.4 KB gzip (core 7.3 · React integration 10.7 · overlay 10.1) | | Runtime dependencies | none |

Percentage overhead depends on what you instrument: wrapping every trivial leaf in a 5000-node tree is expensive, wrapping the twenty components you're investigating is not. The benchmark reports both, honestly.


Does acting on it help?

Identical interaction, same UI, with and without the three fixes the tool reports (node bench/before-after.mjs):

| | before | after | | --- | ---: | ---: | | renders | 421 | 30 | | potentially avoidable | 370 | 18 | | remounts | 10 | 0 |

Row went from 400 renders to 0 once memo could actually hold. Render time fell only 24%, because these rows are trivial — the counts are exact, the milliseconds are indicative, and BENCHMARKS.md explains why that gap is the honest result rather than a disappointing one.

Limitations

Read docs/FEASIBILITY.md — it classifies every feature as reliable, inferred, or impossible without React internals, and this package uses no private React APIs.

The headlines:

  • Parent means nearest instrumented ancestor. Uninstrumented components in between are reported as such, not glossed over.
  • Context subscriptions are not enumerable at runtime. Context-driven renders are correlation within one commit, reported at medium confidence, and only for contexts you track.
  • State values are not readable without internals. An untracked state render is reported as state-or-external — we say we cannot tell which, rather than guessing.
  • useTrackedState must be called in an instrumented component. A hook cannot see its own caller, only the nearest instrumented ancestor.
  • Source locations come from the build plugin. Without it there is no runtime way to get them — _debugSource was removed in React 19.
  • Self duration is an upper bound: React exposes subtree time, and we subtract the instrumented descendants we know about.

React support

React 16.9+ (Profiler, context, refs — all public API). Tested against React 18 and 19; see docs/COMPATIBILITY.md for the one behavioural difference between them.

License

MIT