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 +21 -0
- package/README.md +278 -0
- package/dist/chunk-7FTXUOB6.cjs +1030 -0
- package/dist/chunk-7FTXUOB6.cjs.map +1 -0
- package/dist/chunk-EGA344XG.js +113 -0
- package/dist/chunk-EGA344XG.js.map +1 -0
- package/dist/chunk-GRPJ4JAB.js +1012 -0
- package/dist/chunk-GRPJ4JAB.js.map +1 -0
- package/dist/chunk-OCW2Q3UW.cjs +116 -0
- package/dist/chunk-OCW2Q3UW.cjs.map +1 -0
- package/dist/core.cjs +76 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +198 -0
- package/dist/core.d.ts +198 -0
- package/dist/core.js +3 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +417 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +153 -0
- package/dist/index.d.ts +153 -0
- package/dist/index.js +370 -0
- package/dist/index.js.map +1 -0
- package/dist/overlay.cjs +147 -0
- package/dist/overlay.cjs.map +1 -0
- package/dist/overlay.d.cts +15 -0
- package/dist/overlay.d.ts +15 -0
- package/dist/overlay.js +145 -0
- package/dist/overlay.js.map +1 -0
- package/dist/types-CY61yfhK.d.cts +225 -0
- package/dist/types-CY61yfhK.d.ts +225 -0
- package/package.json +88 -0
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
|
+
[](https://www.npmjs.com/package/react-render-detective)
|
|
4
|
+
[](docs/BUNDLE-SIZE.md)
|
|
5
|
+
[](package.json)
|
|
6
|
+
[](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
|