react-render-pulse 1.0.0 → 1.0.2

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.
Files changed (2) hide show
  1. package/README.md +269 -37
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -4,9 +4,18 @@
4
4
  [![CI](https://github.com/Amalmktk/react-render-pulse/actions/workflows/ci.yml/badge.svg)](https://github.com/Amalmktk/react-render-pulse/actions/workflows/ci.yml)
5
5
  [![license](https://img.shields.io/npm/l/react-render-pulse)](https://github.com/Amalmktk/react-render-pulse/blob/main/LICENSE)
6
6
 
7
- Real-user render performance monitoring for React, at the component level. Wrap the parts of your tree you care about and get render timings from real sessions: render counts, slow renders, p50/p75/p95 and the slowest renders per area and route, plus long tasks attributed to the area that was rendering.
7
+ **Find the React components that slow your app down — on real users' devices.**
8
8
 
9
- Under 3.75 kB brotli. No runtime dependencies besides React 18 or 19.
9
+ React DevTools shows how your app performs on your machine. react-render-pulse shows how it performs for your users: it measures named areas of your component tree in production, summarizes the timings in the browser, and sends small batches to an endpoint you control. You get render counts, slow renders, p50/p75/p95 and the slowest renders per area and route, plus long tasks blamed on the area that was rendering, by device class and app version.
10
+
11
+ - About **10 µs per measured render** in lightweight mode, and **3.6 kB** (brotli) for the client
12
+ - **Privacy-friendly:** no user IDs, IP addresses, page text, input values or query strings
13
+ - **Never breaks your app:** every internal error is caught; missing browser APIs are handled
14
+ - React 18 and 19, ESM and CommonJS, fully typed, `"use client"` ready for Next.js
15
+
16
+ 📖 **Docs:** https://amalmktk.github.io/react-render-pulse/
17
+
18
+ **Contents:** [Install](#install) · [Quick start](#quick-start) · [How it works](#how-it-works) · [Collecting results](#collecting-results) · [API](#api) · [Modes](#modes) · [Frameworks](#frameworks) · [What gets sent](#what-gets-sent) · [Performance](#performance) · [Limitations](#limitations)
10
19
 
11
20
  ## Install
12
21
 
@@ -14,84 +23,307 @@ Under 3.75 kB brotli. No runtime dependencies besides React 18 or 19.
14
23
  npm install react-render-pulse
15
24
  ```
16
25
 
17
- ## Usage
26
+ React 18 or 19 is a peer dependency. There are no other runtime dependencies.
18
27
 
19
- Start it once, in the browser, before your app renders:
28
+ ## Quick start
29
+
30
+ **1. Start it once,** in the browser, before your app renders:
20
31
 
21
32
  ```tsx
33
+ // src/main.tsx
22
34
  import { createRoot } from "react-dom/client";
23
35
  import { initRenderPulse } from "react-render-pulse";
24
36
  import { App } from "./App";
25
37
 
26
38
  initRenderPulse({
27
- endpoint: "/api/render-pulse", // leave out to log batches to the console
39
+ endpoint: "/api/render-pulse", // leave out to log batches to the console instead
28
40
  sampleRate: 0.1, // 10% of page loads
29
- appVersion: import.meta.env.VITE_APP_VERSION,
41
+ appVersion: import.meta.env.VITE_APP_VERSION, // compare releases
30
42
  });
31
43
 
32
44
  createRoot(document.getElementById("root")!).render(<App />);
33
45
  ```
34
46
 
35
- Wrap areas you want timings for:
47
+ **2. Wrap the areas you care about.** Areas can be nested:
36
48
 
37
49
  ```tsx
38
- import { RenderMonitor, useRenderMonitor } from "react-render-pulse";
50
+ import { RenderMonitor } from "react-render-pulse";
39
51
 
40
52
  export function OrdersPage() {
41
53
  return (
42
- <RenderMonitor id="orders-table">
43
- <OrdersTable />
54
+ <RenderMonitor id="orders-page">
55
+ <Filters />
56
+ <RenderMonitor id="orders-table">
57
+ <OrdersTable />
58
+ </RenderMonitor>
44
59
  </RenderMonitor>
45
60
  );
46
61
  }
62
+ ```
63
+
64
+ Or track a single component with the hook:
65
+
66
+ ```tsx
67
+ import { useRenderMonitor } from "react-render-pulse";
47
68
 
48
69
  function PriceChart() {
49
- useRenderMonitor("price-chart"); // exact render count; duration is an upper bound
70
+ useRenderMonitor("price-chart");
50
71
  return <Chart />;
51
72
  }
52
73
  ```
53
74
 
54
- `<RenderMonitor>` adds no DOM elements and never remounts its children. Errors inside the package never reach your app.
75
+ **3. Try it.** Without an `endpoint`, use the page, then switch tabs: the batch is logged to the console as a table, slowest areas first. With an `endpoint`, it's `POST`ed there; see [Collecting results](#collecting-results).
55
76
 
56
- Batches are sent when the page is hidden and every 3 minutes while it's visible. Send them to [`react-render-pulse-collector`](https://www.npmjs.com/package/react-render-pulse-collector) and read them with [`react-render-pulse-viewer`](https://www.npmjs.com/package/react-render-pulse-viewer).
77
+ `<RenderMonitor>` adds no DOM elements and never remounts its children, in any mode, whether or not the session is sampled.
57
78
 
58
- ## Key options
79
+ ## How it works
59
80
 
60
- | Option | Default | Description |
61
- | --- | --- | --- |
62
- | `endpoint` | none | Where batches are `POST`ed. Without one, batches are logged to the console. |
63
- | `sampleRate` | `1` | Fraction of page loads that collect anything. |
64
- | `mode` | `"auto"` | `"lightweight"`, `"profiling"`, or `"auto"`: lightweight until React's profiling build is detected. |
65
- | `slowRenderMs` | `16` | Renders slower than this count as slow. |
66
- | `longTaskMs` | `50` | Shorter long tasks and long animation frames are ignored. |
67
- | `appVersion` | none | Your app's version, for comparing releases. |
68
- | `getRoute` | numeric path segments → `:id` | Returns the current route template, e.g. `/orders/:id`. |
69
- | `exporters` | `[]` | Extra receivers, e.g. `createOtelExporter` from `react-render-pulse/otel`. |
81
+ ```
82
+ <RenderMonitor> / useRenderMonitor ──► in-memory aggregator ──► batch ──► your endpoint ──► storage ──► viewer / SQL
83
+ render timings per area + route: sent on page hide (react-render-pulse-collector)
84
+ PerformanceObserver ──► long tasks histogram, counts, (sendBeacon) and every
85
+ and long animation frames, worst cases 3 minutes (fetch keepalive)
86
+ blamed on the area rendering
87
+ ```
70
88
 
71
- All options: [client API reference](https://github.com/Amalmktk/react-render-pulse/blob/main/docs/api/client.md).
89
+ 1. **Sample:** once per page load, decide whether to measure (`sampleRate`). Unsampled sessions do almost nothing.
90
+ 2. **Measure:** time each monitored area's renders (see [Modes](#modes)) and observe long tasks.
91
+ 3. **Aggregate:** keep counts, a 16-bucket histogram and the 5 slowest renders per area + route, never raw events. Memory stays flat.
92
+ 4. **Send:** when the page is hidden, and every 3 minutes while it's open. Batches are trimmed to 60 KB, sent with `sendBeacon` (falling back to `fetch` with `keepalive`), and retried on `429`/`5xx`.
72
93
 
73
- ## Modes
94
+ Full details: [Architecture](https://amalmktk.github.io/react-render-pulse/guide/architecture).
95
+
96
+ ## Collecting results
97
+
98
+ Batches need somewhere to go. You can use the companion packages, your own endpoint, or OpenTelemetry.
99
+
100
+ **With [`react-render-pulse-collector`](https://www.npmjs.com/package/react-render-pulse-collector)**, a standard `Request` → `Response` handler that validates and stores batches. On Next.js:
101
+
102
+ ```ts
103
+ // app/api/render-pulse/route.ts
104
+ import { createCollector } from "react-render-pulse-collector";
105
+ import { postgresStorage } from "react-render-pulse-collector/postgres";
106
+
107
+ const collect = createCollector({ storage: postgresStorage({ url: process.env.DATABASE_URL! }) });
108
+
109
+ export const POST = collect;
110
+ export const OPTIONS = collect;
111
+ ```
112
+
113
+ It also runs on Vercel, Netlify, Cloudflare Workers and Express, and stores to Postgres or a JSON Lines file.
114
+
115
+ **Read them with [`react-render-pulse-viewer`](https://www.npmjs.com/package/react-render-pulse-viewer):**
116
+
117
+ ```bash
118
+ npx react-render-pulse-viewer report --db "$DATABASE_URL" --since 7d # terminal tables
119
+ npx react-render-pulse-viewer serve --db "$DATABASE_URL" # web viewer on 127.0.0.1:4173
120
+ ```
121
+
122
+ **Or query Postgres yourself.** Each batch is one row with the payload as `jsonb`:
123
+
124
+ ```sql
125
+ SELECT a->>'id' AS area, a->>'route' AS route,
126
+ sum((a->>'renders')::int) AS renders,
127
+ round(100.0 * sum((a->>'slow')::int) / nullif(sum((a->>'renders')::int), 0), 1) AS slow_pct
128
+ FROM render_pulse_batches b
129
+ CROSS JOIN LATERAL jsonb_array_elements(b.payload->'areas') AS a
130
+ WHERE b.received_at > now() - interval '7 days'
131
+ GROUP BY 1, 2
132
+ ORDER BY slow_pct DESC NULLS LAST;
133
+ ```
134
+
135
+ **Or send to OpenTelemetry** with `createOtelExporter` (below). No collector is needed.
136
+
137
+ Step-by-step guide, more SQL, and troubleshooting: [Collecting results](https://amalmktk.github.io/react-render-pulse/guide/collecting-results).
138
+
139
+ ## API
140
+
141
+ ### `initRenderPulse(options?)`
142
+
143
+ Starts measuring. Call it once, in the browser, before your app renders. Calling it again replaces the previous setup. On the server it does nothing.
144
+
145
+ | Option | Type | Default | Description |
146
+ | --- | --- | --- | --- |
147
+ | `endpoint` | `string` | — | Where batches are `POST`ed. Without one, batches only go to `exporters` and the console. |
148
+ | `sampleRate` | `number` (0–1) | `1` | Fraction of page loads that measure anything, decided once per page load. |
149
+ | `mode` | `"auto" \| "lightweight" \| "profiling"` | `"auto"` | How renders are measured. See [Modes](#modes). |
150
+ | `slowRenderMs` | `number` | `16` | Renders slower than this count as slow (one frame at 60 Hz). |
151
+ | `longTaskMs` | `number` | `50` | Shorter long tasks and long animation frames are ignored. |
152
+ | `appVersion` | `string` | — | Your app's version, for comparing releases. |
153
+ | `getRoute` | `() => string` | path with numeric segments → `:id` | The current route, ideally a template like `/orders/:id`. |
154
+ | `flushIntervalMs` | `number` | `180000` | Also send this often while the page is open. `0` sends only when it's hidden. |
155
+ | `maxPayloadBytes` | `number` | `60000` | Largest batch; bigger ones are trimmed. Capped at 65536. |
156
+ | `maxAreas` | `number` | `200` | Most area + route pairs per batch. Capped at 1000. |
157
+ | `worstCases` | `number` | `5` | Slowest renders kept per area per batch. Capped at 50. |
158
+ | `exporters` | `Exporter[]` | — | Receive measurements and batches, e.g. `createOtelExporter`. |
159
+ | `debug` | `boolean` | `true` without `endpoint` | Log each batch to the console, and internal errors as warnings. |
160
+
161
+ Returns `{ sampled, mode, flush(), destroy() }`:
162
+
163
+ - `sampled`: whether this page load measures anything.
164
+ - `mode`: the current mode; in `"auto"` it changes to `"profiling"` once React's profiling build is detected.
165
+ - `flush()`: sends a batch now and returns it, or `null` if nothing was measured.
166
+ - `destroy()`: stops observing and sending. Monitors keep rendering their children.
167
+
168
+ ### `<RenderMonitor id children />`
169
+
170
+ Measures the renders of its children as one area named `id`. Areas with the same `id` are reported together. Nested areas are reported separately, and an outer area's time includes its inner ones.
171
+
172
+ ### `useRenderMonitor(id)`
173
+
174
+ Measures the component that calls it. Render counts are exact. The duration runs from the hook to the commit, so it's an upper bound that can include components rendered after it in the same update.
175
+
176
+ ### Exporters
177
+
178
+ An exporter receives data as it's measured:
179
+
180
+ ```ts
181
+ interface Exporter {
182
+ onRender?: (event: RenderEvent) => void; // every measured render
183
+ onLongTask?: (event: LongTaskEvent) => void; // every long task / long animation frame
184
+ onFlush?: (payload: Payload) => void; // every batch
185
+ }
186
+ ```
187
+
188
+ Built in:
74
189
 
75
- - **Lightweight** (any production build): times each area between two marker components. About 10–15 µs per measured render. Doesn't see updates that start inside the area.
76
- - **Profiling** (React's profiling build): React's `<Profiler>` timings for every commit. Exact, but the profiling build itself costs more; use it for a sampled group.
190
+ - `createConsoleExporter()` logs each batch as a table. It's used automatically when `debug` is on.
191
+ - `createOtelExporter({ meter, slowRenderMs? })` from `react-render-pulse/otel` records OpenTelemetry metrics: `render_pulse.render.duration` and `render_pulse.longtask.duration` (histograms, ms) and `render_pulse.render.slow` (counter), with area, route, phase and mode attributes.
77
192
 
78
- Enable the profiling build by aliasing `react-dom/client` to `react-dom/profiling` (Vite) or with `next build --profile`. See [Modes](https://github.com/Amalmktk/react-render-pulse/blob/main/docs/guide/modes.md).
193
+ ```ts
194
+ import { metrics } from "@opentelemetry/api";
195
+ import { createOtelExporter } from "react-render-pulse/otel";
79
196
 
80
- ## Entry points
197
+ initRenderPulse({ exporters: [createOtelExporter({ meter: metrics.getMeter("my-app") })] });
198
+ ```
199
+
200
+ ### Entry points
81
201
 
82
202
  | Import | Contents |
83
203
  | --- | --- |
84
- | `react-render-pulse` | `initRenderPulse`, `RenderMonitor`, `useRenderMonitor`, `createConsoleExporter`, types. Marked `"use client"`. |
85
- | `react-render-pulse/core` | Types, `HISTOGRAM_BOUNDS`, `histogramPercentile`. No React; safe on servers. |
86
- | `react-render-pulse/otel` | `createOtelExporter`, for OpenTelemetry metrics. |
204
+ | `react-render-pulse` | `initRenderPulse`, `RenderMonitor`, `useRenderMonitor`, `createConsoleExporter`, `HISTOGRAM_BOUNDS`, `histogramPercentile`, and all types. Marked `"use client"`. |
205
+ | `react-render-pulse/core` | Types, `HISTOGRAM_BOUNDS`, `histogramPercentile`. No React; safe in servers and Server Components. |
206
+ | `react-render-pulse/otel` | `createOtelExporter`. |
207
+
208
+ Types: `Payload`, `AreaReport`, `LongTaskReport`, `SessionInfo`, `SessionSummary`, `DeviceContext`, `WorstCase`, `RenderEvent`, `LongTaskEvent`, `Exporter`, `Mode`, `ModeOption`, `RenderPhase`, `RenderPulseOptions`, `RenderPulse`, `RenderMonitorProps`.
209
+
210
+ Full reference: [Client API](https://amalmktk.github.io/react-render-pulse/api/client).
211
+
212
+ ## Modes
213
+
214
+ | | Lightweight | Profiling |
215
+ | --- | --- | --- |
216
+ | How it measures | Two marker components around each area time its render | React's `<Profiler>` reports each commit's render time |
217
+ | Needs | Any production build | React's profiling build |
218
+ | Sees updates that start inside an area | No: wrap the component that owns the state, or use `useRenderMonitor` in it | Yes |
219
+ | Cost | ~10 µs per measured render | About the same as a bare `<Profiler>`, which is the profiling build's own cost |
220
+ | Use for | Every production user | A sampled group or staging |
221
+
222
+ `mode: "auto"` (the default) starts in lightweight mode and switches to profiling mode when React's profiling build is detected. To enable the profiling build:
223
+
224
+ ```ts
225
+ // vite.config.ts: `vite build --mode profiling`
226
+ export default defineConfig(({ mode }) => ({
227
+ plugins: [react()],
228
+ resolve: { alias: mode === "profiling" ? { "react-dom/client": "react-dom/profiling" } : {} },
229
+ }));
230
+ ```
231
+
232
+ ```bash
233
+ next build --profile # Next.js
234
+ ```
235
+
236
+ More: [Modes](https://amalmktk.github.io/react-render-pulse/guide/modes).
237
+
238
+ ## Frameworks
239
+
240
+ **Vite / any SPA:** call `initRenderPulse` in your entry file before `createRoot(...).render(...)`.
241
+
242
+ **Next.js App Router:** use `instrumentation-client.ts` at the project root, which runs before hydration. `<RenderMonitor>` can be used directly in Server Components, because the package is marked `"use client"`.
243
+
244
+ ```ts
245
+ // instrumentation-client.ts
246
+ import { initRenderPulse } from "react-render-pulse";
247
+
248
+ initRenderPulse({
249
+ endpoint: "/api/render-pulse",
250
+ getRoute: () => location.pathname.replace(/^\/orders\/[^/]+/, "/orders/[id]"),
251
+ });
252
+ ```
253
+
254
+ **Next.js Pages Router:** call it at the top level of `pages/_app.tsx`, inside `if (typeof window !== "undefined")`.
255
+
256
+ **React Router:** report route patterns with `matchRoutes`:
257
+
258
+ ```ts
259
+ getRoute: () => {
260
+ const pattern = (matchRoutes(routes, router.state.location) ?? [])
261
+ .map((m) => m.route.path ?? "")
262
+ .filter(Boolean)
263
+ .join("/")
264
+ .replace(/\/+/g, "/");
265
+ return pattern.startsWith("/") ? pattern : `/${pattern}`;
266
+ },
267
+ ```
268
+
269
+ Full setups, with working example apps: [Frameworks](https://amalmktk.github.io/react-render-pulse/guide/frameworks) · [examples/](https://github.com/Amalmktk/react-render-pulse/tree/main/examples).
270
+
271
+ ## What gets sent
272
+
273
+ One JSON object per batch (trimmed here):
274
+
275
+ ```json
276
+ {
277
+ "v": 1,
278
+ "session": { "id": "5f0c9a1e-…", "seq": 1, "mode": "lightweight", "route": "/orders", "appVersion": "2.4.0" },
279
+ "device": { "memoryGb": 4, "cores": 8, "connection": "4g", "viewport": "390x844" },
280
+ "summary": { "renders": 42, "slowRenders": 5, "longTasks": 3, "longTaskMs": 400 },
281
+ "areas": [
282
+ { "id": "orders-table", "route": "/orders", "renders": 42, "slow": 5, "p50": 6.1, "p75": 11.8, "p95": 38.2, "max": 61.2, "hist": [] }
283
+ ],
284
+ "longTasks": [{ "area": "orders-table", "route": "/orders", "count": 3, "maxMs": 212 }]
285
+ }
286
+ ```
287
+
288
+ - **Never collected:** user IDs, IP addresses, cookies, page text, input values, query strings.
289
+ - **The session id** is random per page load, held in memory only, and can't identify a user.
290
+ - **Routes** come from `location.pathname` via `getRoute`. The default only replaces numeric segments, so normalize UUIDs, usernames or slugs in `getRoute`.
291
+
292
+ Every field: [What we collect](https://amalmktk.github.io/react-render-pulse/guide/what-we-collect).
293
+
294
+ ## Performance
295
+
296
+ Measured in Chromium with a benchmark that re-renders a whole app (8 monitored areas) per update, in paired A/B blocks:
297
+
298
+ | Mode | Added per measured render | Accuracy (known 2 / 8 / 20 ms components) |
299
+ | --- | --- | --- |
300
+ | Lightweight (production build) | 0.006–0.014 ms | 2.0 / 8.0 / 20.0 ms |
301
+ | Profiling (profiling build) | ~0.07–0.08 ms, the same as a bare `<Profiler>` | 2.0 / 8.0 / 20.1 ms |
302
+
303
+ The client is 3.6 kB brotli (`initRenderPulse` + `RenderMonitor` + `useRenderMonitor`). CI fails a pull request that exceeds the overhead or size budget. Method and reproduction: [Performance](https://amalmktk.github.io/react-render-pulse/guide/performance).
304
+
305
+ ## Limitations
306
+
307
+ - Lightweight mode doesn't see updates that start below an area, such as a child's own state change. Monitor the component that owns the state, or use profiling mode for those sessions.
308
+ - `useRenderMonitor` durations are an upper bound. Use `<RenderMonitor>` around the component for its own render time.
309
+ - Long tasks are observed only where the browser supports them: Chromium for long tasks and long animation frames. Other browsers still report render timings.
310
+ - Profiling mode needs React's profiling build, which is slower than the regular build. Ship it to a sample, not everyone.
311
+
312
+ ## What's next
313
+
314
+ Planned for v1.5 (no dates; plans can change):
87
315
 
88
- ## Privacy
316
+ - Slow interactions (INP) attributed to the component areas that caused them
317
+ - Release comparisons: which areas got faster or slower between two app versions
318
+ - Render budgets checked in pull requests, with a GitHub Action
319
+ - Regression alerts by webhook, and adaptive sampling (more data from slow devices)
89
320
 
90
- No user IDs, IP addresses, page text, input values or query strings. The session id is random per page load and never stored. Routes come from `location.pathname`; numeric segments are replaced by default, but pass a `getRoute` if paths can contain names, slugs or UUIDs. See [What we collect](https://github.com/Amalmktk/react-render-pulse/blob/main/docs/guide/what-we-collect.md).
321
+ Full list: [ROADMAP.md](https://github.com/Amalmktk/react-render-pulse/blob/main/ROADMAP.md). Ideas and feedback are welcome in [issues](https://github.com/Amalmktk/react-render-pulse/issues).
91
322
 
92
- ## Documentation
323
+ ## Related packages
93
324
 
94
- Full docs: https://github.com/Amalmktk/react-render-pulse/tree/main/docs
325
+ - [`react-render-pulse-collector`](https://www.npmjs.com/package/react-render-pulse-collector): validates and stores batches (Postgres, JSON file, custom)
326
+ - [`react-render-pulse-viewer`](https://www.npmjs.com/package/react-render-pulse-viewer): terminal report and web viewer
95
327
 
96
328
  ## License
97
329
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-render-pulse",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Real-user render performance monitoring for React, at the component level.",
5
5
  "keywords": [
6
6
  "react",