react-render-detective 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 React Render Detective contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,278 @@
1
+ # React Render Detective
2
+
3
+ [![npm](https://img.shields.io/npm/v/react-render-detective?color=2f6df6)](https://www.npmjs.com/package/react-render-detective)
4
+ [![bundle](https://img.shields.io/badge/gzip-12.4%20KB-2f6df6)](docs/BUNDLE-SIZE.md)
5
+ [![deps](https://img.shields.io/badge/runtime%20deps-0-2f6df6)](package.json)
6
+ [![license](https://img.shields.io/npm/l/react-render-detective)](LICENSE)
7
+
8
+ **Know *why* your React components render.**
9
+
10
+ 📖 **[Website](https://shubambhasin.github.io/react-render-detective/)** ·
11
+ [Guide](docs/GUIDE.md) · [API](docs/API.md) · [Feasibility report](docs/FEASIBILITY.md) ·
12
+ [Benchmarks](docs/BENCHMARKS.md)
13
+
14
+ > **Status: 0.1.0, early release.** 74 tests pass on React 18 and 19, benchmarks and bundle budgets
15
+ > are green, and the packed package is verified in a clean install for ESM, CJS and TypeScript
16
+ > consumers. Not yet exercised: a real browser (all testing is jsdom) and Suspense / error-boundary
17
+ > edges. Treat it as a preview — issues welcome.
18
+
19
+ Not just:
20
+
21
+ ```text
22
+ UserProfile rendered 47 times.
23
+ ```
24
+
25
+ But:
26
+
27
+ ```text
28
+ UserProfile rendered because `user` changed by reference.
29
+ Its values are identical. Dashboard recreated the object.
30
+ ```
31
+
32
+ Debug React rendering without scattering `console.log` through your components.
33
+
34
+ ---
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install react-render-detective
40
+ ```
41
+
42
+ ```tsx
43
+ import { init } from "react-render-detective";
44
+
45
+ if (process.env.NODE_ENV !== "production") {
46
+ init();
47
+ }
48
+ ```
49
+
50
+ Then wrap the components you're investigating:
51
+
52
+ ```tsx
53
+ import { withRenderDetective } from "react-render-detective";
54
+
55
+ const UserProfile = withRenderDetective(function UserProfile({ user, onSave }) {
56
+ return /* … */;
57
+ });
58
+ ```
59
+
60
+ That's it. No server, no account, no API key, no browser extension, no data leaves your machine.
61
+
62
+ ```text
63
+ â–² [RRD] UserProfile #47 Reason: prop changed (reference only) Changed: user Duration: 8.4ms
64
+ ```
65
+
66
+ Ask for the whole story at any time:
67
+
68
+ ```ts
69
+ import { explain } from "react-render-detective";
70
+ console.log(explain("UserProfile"));
71
+ ```
72
+
73
+ ```text
74
+ UserProfile
75
+
76
+ 47 recorded renders
77
+
78
+ Why?
79
+ 78% of renders followed `user` changing by reference while its contents stayed the same.
80
+
81
+ Breakdown
82
+ props 37 79%
83
+ parent 8 17%
84
+ mount 1 2%
85
+ state-or-external 1 2%
86
+
87
+ Reference-only prop changes
88
+ user 37 79% (object)
89
+ onSave 31 66% (function)
90
+
91
+ Cost
92
+ average 8.4ms
93
+ total 394.8ms
94
+ potentially avoidable 37 render(s), ~310.8ms
95
+
96
+ Next step
97
+ Find where `user` is created in Dashboard and stabilise it (useMemo, or pass the
98
+ primitive fields you use).
99
+
100
+ Confidence: high
101
+ ```
102
+
103
+ ---
104
+
105
+ ## What makes this different
106
+
107
+ It answers **causality**, not counts:
108
+
109
+ | Question | Answer |
110
+ | --- | --- |
111
+ | What rendered? | component, render number, mount vs update |
112
+ | Why? | props · parent · context · state · external store — with the evidence |
113
+ | What changed? | per prop: value change vs *reference-only* change |
114
+ | Where from? | the nearest instrumented ancestor, and whether it re-rendered |
115
+ | How expensive? | subtree duration, and self duration with descendants subtracted |
116
+ | How sure are we? | every diagnosis carries `high` / `medium` / `low` |
117
+ | What next? | an evidence-based suggestion, or nothing |
118
+
119
+ And it refuses to guess. When the runtime cannot tell you why something rendered, it says:
120
+
121
+ ```text
122
+ Cause could not be determined reliably.
123
+ ```
124
+
125
+ Three rules it holds to, which most render-debugging advice does not:
126
+
127
+ 1. **Rendering is not a bug.** Output says *render*, *potentially avoidable render*, *slow render* —
128
+ never "BAD RENDER".
129
+ 2. **Memoization is a trade.** `React.memo` / `useMemo` / `useCallback` are suggested only when the
130
+ evidence supports them, with the cost shown so you can decide.
131
+ 3. **StrictMode is not a 2× regression.** Double-invoked renders are labelled as development
132
+ replays and excluded from every statistic.
133
+
134
+ ---
135
+
136
+ ## Integration modes
137
+
138
+ ### `withRenderDetective` — most accurate
139
+
140
+ ```tsx
141
+ const UserProfile = withRenderDetective(UserProfileImpl, { name: "UserProfile" });
142
+ ```
143
+
144
+ Full diagnosis: per-prop diffing, parent attribution, Profiler timings.
145
+
146
+ ### `<RenderDetective>` — zero refactor
147
+
148
+ ```tsx
149
+ <RenderDetective name="UserProfile">
150
+ <UserProfile />
151
+ </RenderDetective>
152
+ ```
153
+
154
+ Timings and parent propagation, but it only sees the `children` element — it cannot attribute a
155
+ render to an individual prop.
156
+
157
+ ### `useRenderDiagnostics` — from inside
158
+
159
+ ```tsx
160
+ function UserProfile(props) {
161
+ const diagnostics = useRenderDiagnostics("UserProfile", props);
162
+ // …
163
+ }
164
+ ```
165
+
166
+ Catches state-driven renders too, but a hook cannot install a `<Profiler>` around its own
167
+ component, so **no durations** are available in this mode.
168
+
169
+ ### Naming what the runtime can't see
170
+
171
+ ```tsx
172
+ const [items, setItems] = useTrackedState("items", []); // proves a state-driven render
173
+ useTrackedContextValue("AuthContext", value); // inside your provider
174
+ useTrackedEffect("sync", () => { … }, [userId]); // which declared dep changed
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Overlay
180
+
181
+ ```tsx
182
+ if (process.env.NODE_ENV !== "production") {
183
+ const { mountOverlay } = await import("react-render-detective/overlay");
184
+ mountOverlay();
185
+ }
186
+ ```
187
+
188
+ A floating panel with live totals, the most expensive components, and the full `explain()` output
189
+ for whichever one you select. It renders in a shadow DOM outside your React tree — an inspector
190
+ that re-rendered the tree it measures would be measuring itself.
191
+
192
+ The overlay is optional and lazily imported; the core is fully usable from the console.
193
+
194
+ ---
195
+
196
+ ## Configuration
197
+
198
+ ```ts
199
+ init({
200
+ enabled: process.env.NODE_ENV !== "production",
201
+ mode: "console", // "silent" | "console" | "verbose"
202
+ include: [/^Dashboard/], // empty = everything
203
+ exclude: ["Icon", "Button"], // exclude wins over include
204
+ samplingRate: 1, // 0–1, decided once per component instance
205
+ maxEvents: 1000, // bounded ring buffer
206
+ slowRenderThreshold: 16,
207
+ thresholds: { monitor: 5, slow: 16, verySlow: 50, critical: 100 },
208
+ inspection: { depth: 1, maxObjectKeys: 20, maxArrayLength: 20, maxStringLength: 120 },
209
+ compareFunctionSource: false, // spot recreated inline closures (opt-in)
210
+ onEvent: (event) => {},
211
+ });
212
+ ```
213
+
214
+ `init()` is idempotent — Fast Refresh, duplicate module copies and repeated calls reconfigure the
215
+ single instance instead of stacking three copies of the debugger.
216
+
217
+ Full API: [docs/API.md](docs/API.md).
218
+
219
+ ---
220
+
221
+ ## Safety
222
+
223
+ - **Nothing leaves your machine.** No network calls, no telemetry, no analytics, no storage.
224
+ - **Production-safe by default.** `detectDev()` defaults to *off* when it cannot tell.
225
+ With `enabled: false` nothing is registered and no `<Profiler>` is mounted.
226
+ - **Fail-safe.** Every instrumentation path is wrapped: a throwing getter, an exploding subscriber
227
+ or an un-inspectable prop degrades the diagnostic, never your app.
228
+ - **Bounded.** Ring-buffered events, capped inspection depth/width, props released on unmount.
229
+
230
+ ---
231
+
232
+ ## Cost
233
+
234
+ Measured against the built package (`npm run bench`, full numbers in
235
+ [docs/BENCHMARKS.md](docs/BENCHMARKS.md)):
236
+
237
+ | | |
238
+ | --- | --- |
239
+ | Per instrumented component | ~7µs structural + ~7–16µs recording, well inside the 0.1ms target |
240
+ | Bundle, everything loaded | **12.4 KB gzip** (core 7.3 · React integration 10.7 · overlay 10.1) |
241
+ | Runtime dependencies | **none** |
242
+
243
+ Percentage overhead depends on what you instrument: wrapping every trivial leaf in a 5000-node
244
+ tree is expensive, wrapping the twenty components you're investigating is not. The benchmark
245
+ reports both, honestly.
246
+
247
+ ---
248
+
249
+ ## Limitations
250
+
251
+ Read [docs/FEASIBILITY.md](docs/FEASIBILITY.md) — it classifies every feature as reliable,
252
+ inferred, or impossible without React internals, and this package uses **no private React APIs**.
253
+
254
+ The headlines:
255
+
256
+ - **Parent** means *nearest instrumented ancestor*. Uninstrumented components in between are
257
+ reported as such, not glossed over.
258
+ - **Context** subscriptions are not enumerable at runtime. Context-driven renders are correlation
259
+ within one commit, reported at medium confidence, and only for contexts you track.
260
+ - **State values** are not readable without internals. An untracked state render is reported as
261
+ `state-or-external` — we say we cannot tell which, rather than guessing.
262
+ - **`useTrackedState` must be called in an instrumented component.** A hook cannot see its own
263
+ caller, only the nearest instrumented ancestor.
264
+ - **Source locations** need a build-time transform (`_debugSource` was removed in React 19), so
265
+ they are not in v1.
266
+ - **Self duration** is an upper bound: React exposes subtree time, and we subtract the instrumented
267
+ descendants we know about.
268
+
269
+ ---
270
+
271
+ ## React support
272
+
273
+ React 16.9+ (`Profiler`, context, refs — all public API). Tested against React 18 and 19; see
274
+ [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md) for the one behavioural difference between them.
275
+
276
+ ## License
277
+
278
+ MIT