Skip to content

Repository files navigation

react-render-sentinel

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

Packages

PackageWhat it does
@react-render-sentinel/coreFramework-agnostic engine: snapshot comparison, reason classification, optimization scoring, cascade/aggregate reporting. No React dependency.
@react-render-sentinel/reactReact integration: <RenderSentinel> provider, withRenderSentinel HOC, useRenderSentinel hook.
@react-render-sentinel/console-reporterFormats render events, cascades, and aggregated reports for the console.
@react-render-sentinel/devtools, @react-render-sentinel/cliReserved for future work — not implemented in v1 (see Non-goals).

Quick start

npm i @bavecs/react-render-sentinel

The 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/* ... */;}

What it looks like

⚠ 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)

Configuration

<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.

Design principles this implementation follows

  • 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 the UNKNOWN / UNKNOWN_CHANGE categories.
  • False positives are worse than false negatives for a "should I care" tool. Negligible-duration renders are forced to LOW priority regardless of how unstable their props look (see analyzeRender'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 as UserCard { instances: 500, avoidable: 420, ... }, not 500 separate log lines.

Non-goals (v1)

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).

Monorepo layout

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/

Development

pnpm install
pnpm build # turbo run build across all packages
pnpm typecheck # turbo run typecheck across all packagescd examples/vite-react && pnpm dev

What's implemented vs. spec

Everything 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

About

A React developer tool that monitors component re-renders, explains their causes, and identifies avoidable render cascades.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages