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/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/index.cjs +628 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +179 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +601 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
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
|