React render observability that explains what changed, why it rendered, and whether you should care.
A development-time debugging library that watches component renders, figures out what changed, tries to identify why, scores whether it's worth caring about, and reports the results — without screaming "useMemo everything" at every render, because a re-render is not, by itself, a bug.
Render detected
↓
Was something actually changed?
↓
Is the render potentially avoidable?
↓
Did it cause meaningful work?
↓
Is it worth optimizing?
↓
Explain + prioritize
| Package | What it does |
|---|---|
@react-render-sentinel/core | Framework-agnostic engine: snapshot comparison, reason classification, optimization scoring, cascade/aggregate reporting. No React dependency. |
@react-render-sentinel/react | React integration: <RenderSentinel> provider, withRenderSentinel HOC, useRenderSentinel hook. |
@react-render-sentinel/console-reporter | Formats render events, cascades, and aggregated reports for the console. |
@react-render-sentinel/devtools, @react-render-sentinel/cli | Reserved for future work — not implemented in v1 (see Non-goals). |
npm i @bavecs/react-render-sentinelThe package is available on npm as @bavecs/react-render-sentinel.
Simplest possible setup — wrap your app root:
import{createRoot}from"react-dom/client";import{RenderSentinel}from"@react-render-sentinel/react";createRoot(document.getElementById("root")!).render(<RenderSentinelenabled={import.meta.env.DEV}><App/></RenderSentinel>);Or instrument a single component without any provider at all (the lightest-weight entry point):
import{withRenderSentinel}from"@react-render-sentinel/react";functionUserTable(props: UserTableProps){/* ... */}exportdefaultwithRenderSentinel(UserTable,{name: "UserTable"});Or use the hook form inside a component:
import{useRenderSentinel}from"@react-render-sentinel/react";functionUserTable(props: UserTableProps){useRenderSentinel({name: "UserTable", props,trackProps: true});return/* ... */;}⚠ Render Sentinel - UserList (#42)
Duration: 12.80ms
Why it rendered: Prop "filters" received a new reference, but its contents
are structurally equivalent to the previous render.
"filters" received a new reference:
Previous: { "status": "active", "role": "admin" }
Current: { "status": "active", "role": "admin" }
Analysis: values are structurally equivalent.
Impact: Potentially avoidable
Priority: HIGH - 86/100
Render path: Dashboard -> UserList
Renders that aren't worth caring about get a single quiet line instead of a wall of red:
ℹ Icon - Prop reference changed, but render duration was negligible. (priority: LOW, no action recommended)
<RenderSentinelenabledmode="smart"// "all" | "smart" (default) | "strict"threshold={1}include={["UserList","UserCard"]}exclude={["Icon"]}reporter="console"trackDurationdetectCascadesstrictModeHandling="smart"// "smart" | "report" | "ignore"><App/></RenderSentinel>See RenderSentinelOptions for the full list, including comparison
limits (maxDepth, maxKeys, maxArrayLength, timeoutMs) that bound structural comparison so the
debugging tool can never cause a bigger performance problem than the one it's investigating.
- Prefer "unknown" over a misleading diagnostic. The classifier and function-reference comparator
never claim certainty they don't have — see
FunctionReferenceChange.certainty(capped at"medium") and theUNKNOWN/UNKNOWN_CHANGEcategories. - False positives are worse than false negatives for a "should I care" tool. Negligible-duration
renders are forced to
LOWpriority regardless of how unstable their props look (seeanalyzeRender's duration guard). - Never do more work than necessary to answer the question. Comparison escalates through
reference → primitive → bounded structural equality, and bails out (reporting low confidence
rather than lying) once it hits
maxDepth/maxKeys/maxArrayLength/timeoutMs. - A component-type view and a component-instance view are different questions. 500
<UserCard>instances are reported asUserCard { instances: 500, avoidable: 420, ... }, not 500 separate log lines.
This is not: a React DevTools replacement, a full profiler replacement, a state management
library, a React compiler, a linter, or an automatic code modifier. It observes and explains; it does
not rewrite your code (there is no react-render-sentinel optimize and there never will be in v1).
react-render-sentinel/
├── packages/
│ ├── core/ framework-agnostic engine
│ ├── react/ React integration (provider, HOC, hook)
│ ├── console-reporter/ console output formatting
│ ├── devtools/ reserved, not yet implemented
│ └── cli/ reserved, not yet implemented
├── examples/
│ └── vite-react/ runnable example (Dashboard → UserList → UserCard)
└── docs/
pnpm install
pnpm build # turbo run build across all packages
pnpm typecheck # turbo run typecheck across all packagescd examples/vite-react && pnpm devEverything in spec sections 1–21 has a working implementation: change detection engine (section 9), reason classifier (10), optimization scoring (11), console reporter (12), aggregated reporting (13), cascade detection (4, 14), the HOC/hook/provider APIs (15–17), the bounded snapshot/comparison model (18–19), StrictMode handling (20), and memo-awareness (21).
Not yet implemented (explicitly deferred in the spec too): context analysis (section 22, marked
"v1.1 or v2" in the spec), the devtools and cli packages (scaffolded but empty — the console
reporter is the v1 MVP output per spec section 12), and ref forwarding through withRenderSentinel.
One implementation note worth knowing: the scoring formula's frequency term is a genuine sliding
5-second window, so a component's score will keep climbing as long as it keeps rendering within
that window and settle back down once the render rate drops — this is intentional (it's exactly the
"did this happen a lot recently" signal the formula calls for), not a runaway counter.
Vécsei Balázs