react-dev-profiler 1.0.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/dist/index.cjs ADDED
@@ -0,0 +1,628 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DevProfiler: () => DevProfiler
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/DevProfiler.tsx
28
+ var import_react4 = require("react");
29
+
30
+ // src/DevProfiler.module.css
31
+ var DevProfiler_default = {};
32
+
33
+ // src/types.ts
34
+ var HISTORY_SIZE = 60;
35
+ var INITIAL_PROFILER = {
36
+ phase: "mount",
37
+ actualDuration: 0,
38
+ baseDuration: 0,
39
+ commitCount: 0
40
+ };
41
+ var INITIAL_STATS = {
42
+ domMutations: 0,
43
+ domNodes: 0,
44
+ frameTime: 0,
45
+ frameTimeMin: 0,
46
+ frameTimeMax: 0,
47
+ frameTimeP99: 0,
48
+ frameTimeHistory: [],
49
+ longTasks: 0,
50
+ rendersPerSecond: 0,
51
+ memory: 0,
52
+ dimensions: "\u2013",
53
+ profiler: INITIAL_PROFILER
54
+ };
55
+ function percentile(sorted, p) {
56
+ if (sorted.length === 0) return 0;
57
+ const idx = Math.ceil(p / 100 * sorted.length) - 1;
58
+ return sorted[Math.max(0, idx)];
59
+ }
60
+
61
+ // src/env.ts
62
+ var __DEV__ = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
63
+
64
+ // src/hooks.ts
65
+ var import_react = require("react");
66
+ function useAnchorPosition(ref, position = "bottom-left") {
67
+ const [pos, setPos] = (0, import_react.useState)({ top: 0, left: 0 });
68
+ (0, import_react.useEffect)(() => {
69
+ if (!ref.current) return;
70
+ const update = () => {
71
+ if (!ref.current) return;
72
+ const rect = ref.current.getBoundingClientRect();
73
+ const margin = 8;
74
+ switch (position) {
75
+ case "top-left":
76
+ setPos({ top: rect.top + margin, left: rect.left + margin });
77
+ break;
78
+ case "top-right":
79
+ setPos({ top: rect.top + margin, left: rect.right - margin });
80
+ break;
81
+ case "bottom-right":
82
+ setPos({ top: rect.bottom - margin, left: rect.right - margin });
83
+ break;
84
+ case "bottom-left":
85
+ default:
86
+ setPos({ top: rect.bottom - margin, left: rect.left + margin });
87
+ break;
88
+ }
89
+ };
90
+ const observer = new ResizeObserver(update);
91
+ observer.observe(ref.current);
92
+ observer.observe(document.documentElement);
93
+ return () => observer.disconnect();
94
+ }, [ref, position]);
95
+ return pos;
96
+ }
97
+ function useDraggable() {
98
+ const [offset, setOffset] = (0, import_react.useState)({ x: 0, y: 0 });
99
+ const dragging = (0, import_react.useRef)(false);
100
+ const start = (0, import_react.useRef)({ x: 0, y: 0 });
101
+ const onPointerDown = (0, import_react.useCallback)((e) => {
102
+ dragging.current = true;
103
+ start.current = { x: e.clientX - offset.x, y: e.clientY - offset.y };
104
+ e.target.setPointerCapture(e.pointerId);
105
+ }, [offset]);
106
+ const onPointerMove = (0, import_react.useCallback)((e) => {
107
+ if (!dragging.current) return;
108
+ setOffset({
109
+ x: e.clientX - start.current.x,
110
+ y: e.clientY - start.current.y
111
+ });
112
+ }, []);
113
+ const onPointerUp = (0, import_react.useCallback)(() => {
114
+ dragging.current = false;
115
+ }, []);
116
+ return { offset, handlers: { onPointerDown, onPointerMove, onPointerUp } };
117
+ }
118
+ function useDomTracker(wrapperRef, enabled) {
119
+ const mutations = (0, import_react.useRef)(0);
120
+ const nodeCount = (0, import_react.useRef)(0);
121
+ const dirty = (0, import_react.useRef)(true);
122
+ (0, import_react.useEffect)(() => {
123
+ if (!enabled || !wrapperRef.current) return;
124
+ const observer = new MutationObserver((records) => {
125
+ let realMutation = false;
126
+ for (const record of records) {
127
+ if (record.type === "attributes" && record.attributeName === "class") continue;
128
+ realMutation = true;
129
+ }
130
+ if (realMutation) {
131
+ mutations.current++;
132
+ dirty.current = true;
133
+ }
134
+ });
135
+ observer.observe(wrapperRef.current, {
136
+ childList: true,
137
+ subtree: true,
138
+ attributes: true,
139
+ attributeFilter: ["style"]
140
+ });
141
+ return () => observer.disconnect();
142
+ }, [wrapperRef, enabled]);
143
+ const getNodeCount = (0, import_react.useCallback)(() => {
144
+ if (dirty.current && wrapperRef.current) {
145
+ nodeCount.current = wrapperRef.current.querySelectorAll("*").length;
146
+ dirty.current = false;
147
+ }
148
+ return nodeCount.current;
149
+ }, [wrapperRef]);
150
+ const reset = (0, import_react.useCallback)(() => {
151
+ mutations.current = 0;
152
+ dirty.current = true;
153
+ }, []);
154
+ return { mutations, getNodeCount, reset };
155
+ }
156
+ function useRenderRate() {
157
+ const renderCount = (0, import_react.useRef)(0);
158
+ const rendersPerSecond = (0, import_react.useRef)(0);
159
+ const tick = (0, import_react.useCallback)(() => {
160
+ renderCount.current++;
161
+ }, []);
162
+ (0, import_react.useEffect)(() => {
163
+ const id = setInterval(() => {
164
+ rendersPerSecond.current = renderCount.current;
165
+ renderCount.current = 0;
166
+ }, 1e3);
167
+ return () => clearInterval(id);
168
+ }, []);
169
+ const reset = (0, import_react.useCallback)(() => {
170
+ renderCount.current = 0;
171
+ rendersPerSecond.current = 0;
172
+ }, []);
173
+ return { rendersPerSecond, tick, reset };
174
+ }
175
+ function useRenderFlash(wrapperRef, open) {
176
+ const mutationCount = (0, import_react.useRef)(0);
177
+ (0, import_react.useEffect)(() => {
178
+ if (!open || !wrapperRef.current) return;
179
+ const observer = new MutationObserver(() => {
180
+ mutationCount.current++;
181
+ if (!wrapperRef.current || mutationCount.current <= 1) return;
182
+ const el = wrapperRef.current;
183
+ el.classList.add(DevProfiler_default.flash);
184
+ setTimeout(() => el.classList.remove(DevProfiler_default.flash), 150);
185
+ });
186
+ observer.observe(wrapperRef.current, { childList: true, subtree: true });
187
+ return () => observer.disconnect();
188
+ }, [wrapperRef, open]);
189
+ }
190
+ function useLongTasks(enabled) {
191
+ const count = (0, import_react.useRef)(0);
192
+ (0, import_react.useEffect)(() => {
193
+ if (!enabled || typeof PerformanceObserver === "undefined") return;
194
+ try {
195
+ const observer = new PerformanceObserver((list) => {
196
+ count.current += list.getEntries().length;
197
+ });
198
+ observer.observe({ entryTypes: ["longtask"] });
199
+ return () => observer.disconnect();
200
+ } catch {
201
+ }
202
+ }, [enabled]);
203
+ const reset = (0, import_react.useCallback)(() => {
204
+ count.current = 0;
205
+ }, []);
206
+ return { count, reset };
207
+ }
208
+
209
+ // src/DevStatsPanel.tsx
210
+ var import_react2 = require("react");
211
+ var import_react_dom = require("react-dom");
212
+ var import_jsx_runtime = require("react/jsx-runtime");
213
+ function FrameTimeGraph({ history }) {
214
+ const max = Math.max(33, ...history);
215
+ const w = 140;
216
+ const h = 32;
217
+ const barW = Math.max(1, w / HISTORY_SIZE - 0.5);
218
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: DevProfiler_default.graphWrap, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: w, height: h, style: { display: "block" }, children: [
219
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { width: w, height: h, rx: 3, fill: "#111" }),
220
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
221
+ "line",
222
+ {
223
+ x1: 0,
224
+ y1: h - 16.67 / max * h,
225
+ x2: w,
226
+ y2: h - 16.67 / max * h,
227
+ stroke: "#1a3a1a",
228
+ strokeWidth: 1
229
+ }
230
+ ),
231
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
232
+ "line",
233
+ {
234
+ x1: 0,
235
+ y1: h - 33 / max * h,
236
+ x2: w,
237
+ y2: h - 33 / max * h,
238
+ stroke: "#3a1a1a",
239
+ strokeWidth: 1
240
+ }
241
+ ),
242
+ history.map((ms, i) => {
243
+ const x = i / HISTORY_SIZE * w;
244
+ const barH = Math.min(ms / max * h, h);
245
+ const color = ms > 33 ? "#ef4444" : ms > 16.67 ? "#f59e0b" : "#4ade80";
246
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x, y: h - barH, width: barW, height: barH, fill: color, opacity: 0.8, rx: 0.5 }, i);
247
+ })
248
+ ] }) });
249
+ }
250
+ function StatRow({ label, value, sub, color = "#4ade80" }) {
251
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: DevProfiler_default.row, children: [
252
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: DevProfiler_default.rowLabel, children: label }),
253
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
254
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: DevProfiler_default.rowValue, style: { color }, children: value }),
255
+ sub && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "#444", fontSize: 9, marginLeft: 4 }, children: sub })
256
+ ] })
257
+ ] });
258
+ }
259
+ function getPanelStyle(pos, offset, position) {
260
+ const style = {
261
+ transform: `translate(${offset.x}px, ${offset.y}px)`
262
+ };
263
+ if (position.startsWith("bottom")) {
264
+ style.bottom = window.innerHeight - pos.top;
265
+ } else {
266
+ style.top = pos.top;
267
+ }
268
+ if (position.endsWith("right")) {
269
+ style.right = window.innerWidth - pos.left;
270
+ } else {
271
+ style.left = pos.left;
272
+ }
273
+ return style;
274
+ }
275
+ function DevStatsPanel({
276
+ targetRef,
277
+ domTracker,
278
+ renderRate,
279
+ longTasks,
280
+ profilerData,
281
+ onClose,
282
+ onReset,
283
+ position = "bottom-left",
284
+ instanceId,
285
+ instanceCount = 1
286
+ }) {
287
+ const [stats, setStats] = (0, import_react2.useState)(INITIAL_STATS);
288
+ const [exported, setExported] = (0, import_react2.useState)(false);
289
+ const lastFrame = (0, import_react2.useRef)(performance.now());
290
+ const frameTimeHistory = (0, import_react2.useRef)([]);
291
+ const allFrameTimes = (0, import_react2.useRef)([]);
292
+ const pos = useAnchorPosition(targetRef, position);
293
+ const { offset, handlers: dragHandlers } = useDraggable();
294
+ (0, import_react2.useEffect)(() => {
295
+ let animId;
296
+ let frameCount = 0;
297
+ let frameTotalMs = 0;
298
+ let lastSecond = performance.now();
299
+ const tick = () => {
300
+ const now = performance.now();
301
+ const delta = now - lastFrame.current;
302
+ lastFrame.current = now;
303
+ frameCount++;
304
+ frameTotalMs += delta;
305
+ if (now - lastSecond >= 1e3) {
306
+ const avgFrameTime = frameTotalMs / frameCount;
307
+ const hist = frameTimeHistory.current;
308
+ if (hist.length >= HISTORY_SIZE) hist.shift();
309
+ hist.push(avgFrameTime);
310
+ allFrameTimes.current.push(avgFrameTime);
311
+ const sorted = [...allFrameTimes.current].sort((a, b) => a - b);
312
+ const el = targetRef.current;
313
+ const dims = el ? `${el.offsetWidth} x ${el.offsetHeight}` : "\u2013";
314
+ const perf = performance;
315
+ const mem = perf.memory ? Math.round(perf.memory.usedJSHeapSize / 1024 / 1024) : 0;
316
+ setStats({
317
+ domMutations: domTracker.mutations.current,
318
+ domNodes: domTracker.getNodeCount(),
319
+ frameTime: avgFrameTime,
320
+ frameTimeMin: sorted[0] ?? 0,
321
+ frameTimeMax: sorted[sorted.length - 1] ?? 0,
322
+ frameTimeP99: percentile(sorted, 99),
323
+ frameTimeHistory: [...hist],
324
+ longTasks: longTasks.count.current,
325
+ rendersPerSecond: renderRate.rendersPerSecond.current,
326
+ memory: mem,
327
+ dimensions: dims,
328
+ profiler: { ...profilerData.current }
329
+ });
330
+ frameCount = 0;
331
+ frameTotalMs = 0;
332
+ lastSecond = now;
333
+ }
334
+ animId = requestAnimationFrame(tick);
335
+ };
336
+ animId = requestAnimationFrame(tick);
337
+ return () => cancelAnimationFrame(animId);
338
+ }, [targetRef, domTracker, renderRate, longTasks]);
339
+ const handleReset = (0, import_react2.useCallback)(() => {
340
+ frameTimeHistory.current = [];
341
+ allFrameTimes.current = [];
342
+ setStats(INITIAL_STATS);
343
+ onReset();
344
+ }, [onReset]);
345
+ const handleExport = (0, import_react2.useCallback)(() => {
346
+ const payload = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...stats };
347
+ const json = JSON.stringify(payload, null, 2);
348
+ try {
349
+ navigator.clipboard.writeText(json).then(() => {
350
+ setExported(true);
351
+ setTimeout(() => setExported(false), 1200);
352
+ });
353
+ } catch {
354
+ const ta = document.createElement("textarea");
355
+ ta.value = json;
356
+ ta.style.position = "fixed";
357
+ ta.style.opacity = "0";
358
+ document.body.appendChild(ta);
359
+ ta.select();
360
+ document.execCommand("copy");
361
+ document.body.removeChild(ta);
362
+ setExported(true);
363
+ setTimeout(() => setExported(false), 1200);
364
+ }
365
+ }, [stats]);
366
+ const ftColor = stats.frameTime > 33 ? "#ef4444" : stats.frameTime > 16.67 ? "#f59e0b" : "#4ade80";
367
+ const rpsColor = stats.rendersPerSecond > 30 ? "#ef4444" : stats.rendersPerSecond > 10 ? "#f59e0b" : "#4ade80";
368
+ const actualColor = stats.profiler.actualDuration > 16 ? "#ef4444" : stats.profiler.actualDuration > 8 ? "#f59e0b" : "#4ade80";
369
+ const fps = stats.frameTime > 0 ? Math.round(1e3 / stats.frameTime) : 0;
370
+ const memoGain = stats.profiler.baseDuration > 0 ? Math.round((1 - stats.profiler.actualDuration / stats.profiler.baseDuration) * 100) : 0;
371
+ const p99Color = stats.frameTimeP99 > 33 ? "#ef4444" : stats.frameTimeP99 > 16.67 ? "#f59e0b" : "#4ade80";
372
+ return (0, import_react_dom.createPortal)(
373
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: DevProfiler_default.panel, style: getPanelStyle(pos, offset, position), children: [
374
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
375
+ "div",
376
+ {
377
+ className: DevProfiler_default.panelHeader,
378
+ ...dragHandlers,
379
+ children: [
380
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: DevProfiler_default.panelTitle, children: [
381
+ "Dev Profiler",
382
+ instanceCount > 1 && instanceId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: DevProfiler_default.instanceBadge, children: instanceId })
383
+ ] }),
384
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: DevProfiler_default.headerActions, children: [
385
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
386
+ "button",
387
+ {
388
+ className: `${DevProfiler_default.exportBtn} ${exported ? DevProfiler_default.exportBtnActive : ""}`,
389
+ onClick: handleExport,
390
+ title: exported ? "Copied!" : "Copy stats to clipboard",
391
+ children: exported ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z" }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "currentColor", children: [
392
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25z" }),
393
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25z" })
394
+ ] })
395
+ }
396
+ ),
397
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: DevProfiler_default.resetBtn, onClick: handleReset, title: "Reset counters", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M14 1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4a1 1 0 0 1 0-2h1.6A6 6 0 0 0 2.07 7.5a1 1 0 1 1-1.97-.36A8 8 0 0 1 13 3.35V2a1 1 0 0 1 1-1zM2 15a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h4a1 1 0 0 1 0 2H4.4A6 6 0 0 0 13.93 8.5a1 1 0 1 1 1.97.36A8 8 0 0 1 3 12.65V14a1 1 0 0 1-1 1z" }) }) }),
398
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: DevProfiler_default.closeBtn, onClick: onClose, children: "\u2715" })
399
+ ] })
400
+ ]
401
+ }
402
+ ),
403
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: DevProfiler_default.body, children: [
404
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: DevProfiler_default.section, children: "Rendering" }),
405
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Frame time", value: `${stats.frameTime.toFixed(1)}ms`, sub: `${fps} fps`, color: ftColor }),
406
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrameTimeGraph, { history: stats.frameTimeHistory }),
407
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: DevProfiler_default.miniRow, children: [
408
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
409
+ "min ",
410
+ stats.frameTimeMin.toFixed(1)
411
+ ] }),
412
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
413
+ "max ",
414
+ stats.frameTimeMax.toFixed(1)
415
+ ] }),
416
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { color: p99Color }, children: [
417
+ "p99 ",
418
+ stats.frameTimeP99.toFixed(1)
419
+ ] })
420
+ ] }),
421
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Renders/s", value: String(stats.rendersPerSecond), color: rpsColor }),
422
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Long tasks", value: String(stats.longTasks), color: stats.longTasks > 0 ? "#f59e0b" : "#4ade80" }),
423
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: DevProfiler_default.separator }),
424
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: DevProfiler_default.section, children: "React Profiler" }),
425
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Phase", value: stats.profiler.phase, color: "#888" }),
426
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Render", value: `${stats.profiler.actualDuration.toFixed(2)}ms`, color: actualColor }),
427
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Base (no memo)", value: `${stats.profiler.baseDuration.toFixed(2)}ms`, color: "#888" }),
428
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Memo gain", value: `${memoGain}%`, color: memoGain > 50 ? "#4ade80" : memoGain > 20 ? "#f59e0b" : "#ef4444" }),
429
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Commits", value: String(stats.profiler.commitCount) }),
430
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: DevProfiler_default.separator }),
431
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: DevProfiler_default.section, children: "DOM" }),
432
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Nodes", value: stats.domNodes.toLocaleString() }),
433
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Mutations", value: String(stats.domMutations) }),
434
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "Size", value: stats.dimensions, color: "#888" }),
435
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: DevProfiler_default.separator }),
436
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: DevProfiler_default.section, children: "Memory" }),
437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatRow, { label: "JS Heap", value: stats.memory > 0 ? `${stats.memory} MB` : "N/A" })
438
+ ] }),
439
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: DevProfiler_default.footer, children: "Ctrl+I to toggle" })
440
+ ] }),
441
+ document.body
442
+ );
443
+ }
444
+
445
+ // src/ToggleButton.tsx
446
+ var import_react3 = require("react");
447
+ var import_react_dom2 = require("react-dom");
448
+ var import_jsx_runtime2 = require("react/jsx-runtime");
449
+ function getButtonStyle(pos, position) {
450
+ const style = {};
451
+ if (position.startsWith("bottom")) {
452
+ style.bottom = window.innerHeight - pos.top + 8;
453
+ } else {
454
+ style.top = pos.top + 8;
455
+ }
456
+ if (position.endsWith("right")) {
457
+ style.right = window.innerWidth - pos.left;
458
+ } else {
459
+ style.left = pos.left;
460
+ }
461
+ return style;
462
+ }
463
+ function ToggleButton({
464
+ targetRef,
465
+ onClick,
466
+ position = "bottom-left"
467
+ }) {
468
+ const pos = useAnchorPosition(targetRef, position);
469
+ const [fps, setFps] = (0, import_react3.useState)(0);
470
+ const lastFrame = (0, import_react3.useRef)(performance.now());
471
+ (0, import_react3.useEffect)(() => {
472
+ let animId;
473
+ let count = 0;
474
+ let lastSecond = performance.now();
475
+ const tick = () => {
476
+ const now = performance.now();
477
+ lastFrame.current = now;
478
+ count++;
479
+ if (now - lastSecond >= 1e3) {
480
+ setFps(count);
481
+ count = 0;
482
+ lastSecond = now;
483
+ }
484
+ animId = requestAnimationFrame(tick);
485
+ };
486
+ animId = requestAnimationFrame(tick);
487
+ return () => cancelAnimationFrame(animId);
488
+ }, []);
489
+ const fpsColor = fps < 30 ? "#ef4444" : fps < 55 ? "#f59e0b" : "#4ade80";
490
+ return (0, import_react_dom2.createPortal)(
491
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
492
+ "button",
493
+ {
494
+ className: DevProfiler_default.toggleBtn,
495
+ onClick,
496
+ title: "Dev Profiler (Ctrl+I)",
497
+ style: getButtonStyle(pos, position),
498
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: DevProfiler_default.toggleFps, style: { color: fpsColor }, children: fps })
499
+ }
500
+ ),
501
+ document.body
502
+ );
503
+ }
504
+
505
+ // src/DevProfiler.tsx
506
+ var import_jsx_runtime3 = require("react/jsx-runtime");
507
+ var TOGGLE_EVENT = "devprofiler:toggle";
508
+ var BOUND_KEY = "__devprofiler_bound";
509
+ if (typeof window !== "undefined" && __DEV__ && !window[BOUND_KEY]) {
510
+ window[BOUND_KEY] = true;
511
+ window.addEventListener("keydown", (e) => {
512
+ if ((e.ctrlKey || e.metaKey) && e.key === "i") {
513
+ e.preventDefault();
514
+ window.dispatchEvent(new CustomEvent(TOGGLE_EVENT));
515
+ }
516
+ });
517
+ }
518
+ var instanceCounter = 0;
519
+ var activeInstances = /* @__PURE__ */ new Set();
520
+ function DevProfiler({
521
+ children,
522
+ position = "bottom-left",
523
+ id
524
+ }) {
525
+ const wrapperRef = (0, import_react4.useRef)(null);
526
+ const [open, setOpen] = (0, import_react4.useState)(false);
527
+ const toggle = (0, import_react4.useCallback)(() => setOpen((prev) => !prev), []);
528
+ const instanceId = (0, import_react4.useRef)(id ?? `profiler-${++instanceCounter}`).current;
529
+ (0, import_react4.useEffect)(() => {
530
+ activeInstances.add(instanceId);
531
+ return () => {
532
+ activeInstances.delete(instanceId);
533
+ };
534
+ }, [instanceId]);
535
+ const domTracker = useDomTracker(wrapperRef, open);
536
+ const renderRate = useRenderRate();
537
+ const longTasks = useLongTasks(open);
538
+ useRenderFlash(wrapperRef, open);
539
+ const profilerData = (0, import_react4.useRef)({ ...INITIAL_PROFILER });
540
+ const onRender = (0, import_react4.useCallback)((_id, phase, actualDuration, baseDuration) => {
541
+ profilerData.current = {
542
+ phase,
543
+ actualDuration,
544
+ baseDuration,
545
+ commitCount: profilerData.current.commitCount + 1
546
+ };
547
+ renderRate.tick();
548
+ }, [renderRate]);
549
+ const handleReset = (0, import_react4.useCallback)(() => {
550
+ domTracker.reset();
551
+ renderRate.reset();
552
+ longTasks.reset();
553
+ profilerData.current = { ...INITIAL_PROFILER };
554
+ }, [domTracker, renderRate, longTasks]);
555
+ (0, import_react4.useEffect)(() => {
556
+ const handler = () => setOpen((prev) => !prev);
557
+ window.addEventListener(TOGGLE_EVENT, handler);
558
+ return () => window.removeEventListener(TOGGLE_EVENT, handler);
559
+ }, []);
560
+ if (!__DEV__) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children });
561
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ref: wrapperRef, className: DevProfiler_default.wrapper, children: [
562
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react4.Profiler, { id: "DevProfiler", onRender, children }),
563
+ !open && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ToggleButton, { targetRef: wrapperRef, onClick: toggle, position }),
564
+ open && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
565
+ DevStatsPanel,
566
+ {
567
+ targetRef: wrapperRef,
568
+ domTracker,
569
+ renderRate,
570
+ longTasks,
571
+ profilerData,
572
+ onClose: toggle,
573
+ onReset: handleReset,
574
+ position,
575
+ instanceId,
576
+ instanceCount: activeInstances.size
577
+ }
578
+ )
579
+ ] });
580
+ }
581
+ // Annotate the CommonJS export names for ESM import in node:
582
+ 0 && (module.exports = {
583
+ DevProfiler
584
+ });
585
+ /**
586
+ * @module react-dev-profiler
587
+ * @description Type definitions and constants for the profiler.
588
+ * @author Frederic Denis (billywild87) — https://github.com/billywild87
589
+ * @license MIT
590
+ */
591
+ /**
592
+ * @module react-dev-profiler
593
+ * @description Environment detection — works with Vite, webpack, Next.js, and any bundler.
594
+ * @author Frederic Denis (billywild87) — https://github.com/billywild87
595
+ * @license MIT
596
+ */
597
+ /**
598
+ * @module react-dev-profiler
599
+ * @description Custom hooks that power the profiler's data collection.
600
+ * @author Frederic Denis (billywild87) — https://github.com/billywild87
601
+ * @license MIT
602
+ */
603
+ /**
604
+ * @module react-dev-profiler
605
+ * @description The main stats panel — renders all performance metrics in a floating overlay.
606
+ * @author Frederic Denis (billywild87) — https://github.com/billywild87
607
+ * @license MIT
608
+ */
609
+ /**
610
+ * @module react-dev-profiler
611
+ * @description Minimal floating button that shows the current FPS at a glance.
612
+ * @author Frederic Denis (billywild87) — https://github.com/billywild87
613
+ * @license MIT
614
+ */
615
+ /**
616
+ * @module react-dev-profiler
617
+ * @description Main wrapper component — wraps your app and enables profiling.
618
+ * @author Frederic Denis (billywild87) — https://github.com/billywild87
619
+ * @license MIT
620
+ */
621
+ /**
622
+ * react-dev-profiler
623
+ * Real-time React performance monitoring for development.
624
+ *
625
+ * @author Frederic Denis (billywild87) — https://github.com/billywild87
626
+ * @license MIT
627
+ */
628
+ //# sourceMappingURL=index.cjs.map