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

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

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(
  <RenderSentinel enabled={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";

function UserTable(props: UserTableProps) {
  /* ... */
}

export default withRenderSentinel(UserTable, { name: "UserTable" });

Or use the hook form inside a component:

import { useRenderSentinel } from "@react-render-sentinel/react";

function UserTable(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

<RenderSentinel
  enabled
  mode="smart"        // "all" | "smart" (default) | "strict"
  threshold={1}
  include={["UserList", "UserCard"]}
  exclude={["Icon"]}
  reporter="console"
  trackDuration
  detectCascades
  strictModeHandling="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 packages
cd 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