@piyawat37/performance-overlay 0.1.0 → 0.1.1
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/README.md +29 -7
- package/dist/PerformanceOverlay.d.ts +3 -1
- package/dist/PerformanceOverlay.js +40 -33
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/style.css +9 -0
- package/dist/thresholds.d.ts +14 -0
- package/dist/thresholds.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
# React performance overlay
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An opt-in browser diagnostic overlay for React 18 and 19. Import the component and styles:
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
|
-
import { PerformanceOverlay } from "@
|
|
7
|
-
import "@
|
|
6
|
+
import { PerformanceOverlay } from "@piyawat37/performance-overlay";
|
|
7
|
+
import "@piyawat37/performance-overlay/style.css";
|
|
8
8
|
|
|
9
|
-
// Vite example: the host app decides when the overlay is available.
|
|
10
9
|
<PerformanceOverlay enabled={import.meta.env.DEV} />
|
|
11
10
|
```
|
|
12
11
|
|
|
13
|
-
`enabled` defaults to `false
|
|
12
|
+
`enabled` defaults to `false`. When disabled or collapsed, sampling stops. `defaultOpen` sets the initial expanded state. Sampling pauses in a hidden tab. Measurements remain in the browser; nothing is sent elsewhere.
|
|
14
13
|
|
|
15
|
-
The overlay
|
|
14
|
+
The overlay shows OK, Watch, Investigate, or Unavailable, with suggested investigation steps for Watch and Investigate. It reports FPS, JavaScript heap, DOM element count, and the longest long task and number of long tasks in each sampling interval. A long task occupies the main thread for at least 50 ms; this count and duration are not CPU usage. The overlay cannot measure system CPU or GPU usage. Heap is a non-standard browser API, not total RAM; long tasks and heap are unavailable in some browsers. Sampling and the overlay itself add overhead.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
Default thresholds are **illustrative starting points**, not universal limits. FPS depends on display refresh rates, throttling and power modes; heap and DOM count depend on the app and hardware. Measure a baseline and adjust the thresholds for each app. A long task can be significant even when no critical status appears. Development builds can differ from production.
|
|
17
|
+
|
|
18
|
+
| Metric | Watch | Investigate |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| FPS | below 45 | 25 or below |
|
|
21
|
+
| JS heap | 200 MB or more | 400 MB or more |
|
|
22
|
+
| DOM elements | 1,500 or more | 3,000 or more |
|
|
23
|
+
| Longest long task | 50 ms or more | 200 ms or more |
|
|
24
|
+
|
|
25
|
+
Override thresholds with whole `{ warning, critical }` pairs. FPS is lower-is-worse; the others are higher-is-worse. Use finite positive values and make the critical threshold lower than warning for FPS, higher than warning for other metrics.
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<PerformanceOverlay
|
|
29
|
+
enabled={import.meta.env.DEV}
|
|
30
|
+
thresholds={{
|
|
31
|
+
fps: { warning: 45, critical: 25 },
|
|
32
|
+
heapMb: { warning: 300, critical: 600 },
|
|
33
|
+
nodes: { warning: 2000, critical: 4000 },
|
|
34
|
+
longTaskMs: { warning: 50, critical: 200 },
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
To develop the package: `npm install`, `npm run build`, then `node --test test/*.test.mjs`.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { OverlayThresholds } from "./thresholds.js";
|
|
1
2
|
export type PerformanceOverlayProps = {
|
|
2
3
|
enabled?: boolean;
|
|
3
4
|
defaultOpen?: boolean;
|
|
5
|
+
thresholds?: Partial<OverlayThresholds>;
|
|
4
6
|
};
|
|
5
|
-
export declare function PerformanceOverlay({ enabled, defaultOpen, }: PerformanceOverlayProps): import("react").JSX.Element | null;
|
|
7
|
+
export declare function PerformanceOverlay({ enabled, defaultOpen, thresholds }: PerformanceOverlayProps): import("react").JSX.Element | null;
|
|
@@ -1,53 +1,60 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
-
|
|
3
|
+
import { assess, defaultThresholds } from "./thresholds.js";
|
|
4
|
+
const advice = {
|
|
5
|
+
fps: "Check Performance recording for slow frames, rendering and animations. Compare with this device’s refresh rate.",
|
|
6
|
+
heap: "Compare heap snapshots after repeating the same action; inspect retained objects and detached DOM nodes.",
|
|
7
|
+
nodes: "Inspect large lists, tables and hidden content; consider pagination or virtualization.",
|
|
8
|
+
tasks: "Record Performance and inspect the longest main-thread tasks, scripts and React renders.",
|
|
9
|
+
};
|
|
10
|
+
export function PerformanceOverlay({ enabled = false, defaultOpen = false, thresholds }) {
|
|
4
11
|
const [open, setOpen] = useState(defaultOpen);
|
|
5
12
|
const [sample, setSample] = useState(null);
|
|
13
|
+
const [visible, setVisible] = useState(() => typeof document === "undefined" || !document.hidden);
|
|
14
|
+
const limits = { ...defaultThresholds, ...thresholds };
|
|
6
15
|
useEffect(() => {
|
|
7
|
-
|
|
16
|
+
const update = () => setVisible(!document.hidden);
|
|
17
|
+
document.addEventListener("visibilitychange", update);
|
|
18
|
+
return () => document.removeEventListener("visibilitychange", update);
|
|
19
|
+
}, []);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!enabled || !open || !visible) {
|
|
22
|
+
setSample(null);
|
|
8
23
|
return;
|
|
24
|
+
}
|
|
9
25
|
let frames = 0;
|
|
10
|
-
let frameId = 0;
|
|
11
26
|
let last = performance.now();
|
|
12
27
|
let longTasks = 0;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
28
|
+
let maxTaskMs = 0;
|
|
29
|
+
let frameId = 0;
|
|
30
|
+
const tick = () => { frames += 1; frameId = requestAnimationFrame(tick); };
|
|
17
31
|
frameId = requestAnimationFrame(tick);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
}
|
|
32
|
+
const supported = typeof PerformanceObserver !== "undefined" && PerformanceObserver.supportedEntryTypes?.includes("longtask");
|
|
33
|
+
const observer = supported ? new PerformanceObserver((list) => {
|
|
34
|
+
for (const entry of list.getEntries()) {
|
|
35
|
+
longTasks += 1;
|
|
36
|
+
maxTaskMs = Math.max(maxTaskMs, entry.duration);
|
|
37
|
+
}
|
|
38
|
+
}) : undefined;
|
|
39
|
+
observer?.observe({ entryTypes: ["longtask"] });
|
|
26
40
|
const timer = window.setInterval(() => {
|
|
27
41
|
const now = performance.now();
|
|
28
42
|
const memory = performance.memory;
|
|
29
|
-
setSample({
|
|
30
|
-
fps: Math.round((frames * 1000) / (now - last)),
|
|
31
|
-
heap: memory ? `${(memory.usedJSHeapSize / 1048576).toFixed(0)} MB` : "Unavailable",
|
|
32
|
-
nodes: document.getElementsByTagName("*").length,
|
|
33
|
-
longTasks,
|
|
34
|
-
});
|
|
43
|
+
setSample({ fps: Math.round(frames * 1000 / (now - last)), heapMb: memory ? memory.usedJSHeapSize / 1048576 : null, nodes: document.getElementsByTagName("*").length, longTasks: supported ? longTasks : null, maxTaskMs: supported ? maxTaskMs : null });
|
|
35
44
|
frames = 0;
|
|
36
45
|
longTasks = 0;
|
|
46
|
+
maxTaskMs = 0;
|
|
37
47
|
last = now;
|
|
38
48
|
}, 1000);
|
|
39
|
-
return () => {
|
|
40
|
-
|
|
41
|
-
clearInterval(timer);
|
|
42
|
-
observer?.disconnect();
|
|
43
|
-
};
|
|
44
|
-
}, [enabled, open]);
|
|
49
|
+
return () => { cancelAnimationFrame(frameId); clearInterval(timer); observer?.disconnect(); };
|
|
50
|
+
}, [enabled, open, visible]);
|
|
45
51
|
if (!enabled)
|
|
46
52
|
return null;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const rows = [
|
|
54
|
+
{ key: "fps", label: "Frame rate", value: sample ? `${sample.fps} FPS` : "—", status: assess(sample?.fps ?? null, limits.fps, true) },
|
|
55
|
+
{ key: "heap", label: "JS heap", value: sample?.heapMb != null ? `${sample.heapMb.toFixed(0)} MB` : "Unavailable", status: assess(sample?.heapMb ?? null, limits.heapMb) },
|
|
56
|
+
{ key: "nodes", label: "DOM elements", value: sample ? sample.nodes.toLocaleString() : "—", status: assess(sample?.nodes ?? null, limits.nodes) },
|
|
57
|
+
{ key: "tasks", label: "Longest task / sec", value: sample?.maxTaskMs != null ? `${sample.maxTaskMs.toFixed(0)} ms (${sample.longTasks})` : "Unavailable", status: assess(sample?.maxTaskMs ?? null, limits.longTaskMs) },
|
|
58
|
+
];
|
|
59
|
+
return (_jsxs("aside", { className: "dev-performance", "aria-label": "Development performance monitor", children: [_jsx("button", { type: "button", className: "dev-performance__toggle", "aria-expanded": open, onClick: () => setOpen(!open), children: open ? "Hide performance" : "Performance" }), open && _jsxs("div", { className: "dev-performance__body", children: [!visible && _jsx("p", { children: "Paused while this tab is hidden." }), rows.map((row) => _jsxs("div", { className: "dev-performance__metric", children: [_jsxs("div", { className: "dev-performance__line", children: [_jsx("span", { children: row.label }), _jsx("strong", { children: row.value })] }), _jsx("span", { className: `dev-performance__status dev-performance__status--${row.status}`, children: row.status === "ok" ? "OK" : row.status === "warning" ? "Watch" : row.status === "critical" ? "Investigate" : "Unavailable" }), (row.status === "warning" || row.status === "critical") && _jsx("p", { children: advice[row.key] })] }, row.key)), _jsx("small", { children: "Indicative thresholds, not CPU/GPU usage. FPS varies by device; heap and task support vary by browser." })] })] }));
|
|
53
60
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export type { PerformanceOverlayProps } from "./PerformanceOverlay.js";
|
|
2
1
|
export { PerformanceOverlay } from "./PerformanceOverlay.js";
|
|
2
|
+
export type { PerformanceOverlayProps } from "./PerformanceOverlay.js";
|
|
3
|
+
export { defaultThresholds } from "./thresholds.js";
|
|
4
|
+
export type { OverlayThresholds, Threshold } from "./thresholds.js";
|
package/dist/index.js
CHANGED
package/dist/style.css
CHANGED
|
@@ -45,3 +45,12 @@
|
|
|
45
45
|
margin-top: 8px;
|
|
46
46
|
color: #c5d1dc;
|
|
47
47
|
}
|
|
48
|
+
.dev-performance { width: min(320px, calc(100vw - 24px)); max-height: calc(100vh - 24px); overflow-y: auto; }
|
|
49
|
+
.dev-performance__body .dev-performance__metric { display: block; padding: 6px 0; border-top: 1px solid #536170; }
|
|
50
|
+
.dev-performance__body .dev-performance__line { display: flex; }
|
|
51
|
+
.dev-performance__status { font-weight: 700; }
|
|
52
|
+
.dev-performance__status--ok { color: #8ce5b0; }
|
|
53
|
+
.dev-performance__status--warning { color: #ffdb8a; }
|
|
54
|
+
.dev-performance__status--critical { color: #ffaaaa; }
|
|
55
|
+
.dev-performance__status--unavailable { color: #c5d1dc; }
|
|
56
|
+
.dev-performance__body p { margin: 4px 0; color: #f5f7fa; }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type Threshold = {
|
|
2
|
+
warning: number;
|
|
3
|
+
critical: number;
|
|
4
|
+
};
|
|
5
|
+
export type OverlayThresholds = {
|
|
6
|
+
fps: Threshold;
|
|
7
|
+
heapMb: Threshold;
|
|
8
|
+
nodes: Threshold;
|
|
9
|
+
longTaskMs: Threshold;
|
|
10
|
+
};
|
|
11
|
+
export type Status = "ok" | "warning" | "critical" | "unavailable";
|
|
12
|
+
/** Starting points only; tune them for the app, devices and workload. */
|
|
13
|
+
export declare const defaultThresholds: OverlayThresholds;
|
|
14
|
+
export declare function assess(value: number | null, threshold: Threshold, lowerIsWorse?: boolean): Status;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Starting points only; tune them for the app, devices and workload. */
|
|
2
|
+
export const defaultThresholds = {
|
|
3
|
+
fps: { warning: 45, critical: 25 },
|
|
4
|
+
heapMb: { warning: 200, critical: 400 },
|
|
5
|
+
nodes: { warning: 1500, critical: 3000 },
|
|
6
|
+
longTaskMs: { warning: 50, critical: 200 },
|
|
7
|
+
};
|
|
8
|
+
export function assess(value, threshold, lowerIsWorse = false) {
|
|
9
|
+
if (value === null || !Number.isFinite(value))
|
|
10
|
+
return "unavailable";
|
|
11
|
+
if (lowerIsWorse ? value <= threshold.critical : value >= threshold.critical)
|
|
12
|
+
return "critical";
|
|
13
|
+
if (lowerIsWorse ? value < threshold.warning : value >= threshold.warning)
|
|
14
|
+
return "warning";
|
|
15
|
+
return "ok";
|
|
16
|
+
}
|