@piyawat37/performance-overlay 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -1,17 +1,58 @@
1
- # React performance overlay
1
+ # Performance overlay
2
2
 
3
- A small, opt-in browser performance overlay. React 18 or 19 is a peer dependency.
3
+ An opt-in browser diagnostic overlay for any JavaScript application, with a React 18/19 adapter. Import the component and styles in React:
4
4
 
5
5
  ```tsx
6
- import { PerformanceOverlay } from "@env-platform/performance-overlay";
7
- import "@env-platform/performance-overlay/style.css";
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`; when false, the component renders nothing and collects no measurements. Pass any boolean expression, such as a feature flag or an environment check supplied by the host bundler. `defaultOpen` defaults to `false` and sets the initial expanded state. The user can expand or collapse it; collection stops while collapsed.
12
+ ### JavaScript, Vue, Angular and other frameworks
14
13
 
15
- The overlay samples frame rate, JavaScript heap, DOM element count, and long tasks. Heap is a non-standard browser API, and long tasks are not available in every browser. FPS depends on display refresh rate and may be throttled in background tabs. These are diagnostic estimates, not CPU utilization. Measurements stay in the browser and include the overlay's own overhead. This package does not send data anywhere.
14
+ The vanilla entry point does not depend on React. Import the stylesheet once using your bundler, mount after `document.body` is available, and destroy the overlay when your app or component is disposed:
16
15
 
17
- Build with `npm install && npm run build` in this directory. Publish only after choosing and verifying the package name, scope, and registry.
16
+ ```ts
17
+ import { mountPerformanceOverlay } from "@piyawat37/performance-overlay/vanilla";
18
+ import "@piyawat37/performance-overlay/style.css";
19
+
20
+ const overlay = mountPerformanceOverlay({ enabled: import.meta.env.DEV });
21
+ // Later: overlay.setEnabled(true) or overlay.setEnabled(false);
22
+ // On teardown: overlay.destroy();
23
+ ```
24
+
25
+ Use your own framework's environment flag instead of `import.meta.env.DEV` where applicable. Mount in the browser (for example Vue `onMounted` or Angular `ngAfterViewInit`), and call `destroy()` when unmounting. For custom integrations, `@piyawat37/performance-overlay/core` exports `observePerformance`, `snapshotDom`, `groupChildren`, `assess`, and threshold types. `observePerformance(callback)` returns a stop function. The React adapter keeps the original root import API. React and React DOM are optional peer dependencies; React applications must provide React.
26
+
27
+ `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.
28
+
29
+ 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.
30
+
31
+ 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.
32
+
33
+ | Metric | Watch | Investigate |
34
+ | --- | --- | --- |
35
+ | FPS | below 45 | 25 or below |
36
+ | JS heap | 200 MB or more | 400 MB or more |
37
+ | DOM elements | 1,500 or more | 3,000 or more |
38
+ | Longest long task | 50 ms or more | 200 ms or more |
39
+
40
+ 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.
41
+
42
+ ```tsx
43
+ <PerformanceOverlay
44
+ enabled={import.meta.env.DEV}
45
+ thresholds={{
46
+ fps: { warning: 45, critical: 25 },
47
+ heapMb: { warning: 300, critical: 600 },
48
+ nodes: { warning: 2000, critical: 4000 },
49
+ longTaskMs: { warning: 50, critical: 200 },
50
+ }}
51
+ />
52
+ ```
53
+
54
+ To develop the package: `npm install`, `npm run build`, then `node --test test/*.test.mjs`.
55
+
56
+ ### Explore the DOM tree
57
+
58
+ Select **Explore DOM tree** under DOM elements to capture a snapshot and navigate from `document body` through child groups. Siblings with the same tag, role and ID are grouped; counts include those elements and all their descendants. Each group shows its share of its parent group. Select **Explore** to drill down, a breadcrumb to go back, and **Highlight** to outline up to 50 connected and visible elements. Use **Refresh DOM scan** after the page changes. The overlay is excluded from counts. The snapshot counts hidden descendants and does not include shadow roots or iframe documents. A large group is a lead to investigate, not proof that it is slow.
@@ -1,5 +1,4 @@
1
- export type PerformanceOverlayProps = {
2
- enabled?: boolean;
3
- defaultOpen?: boolean;
4
- };
5
- export declare function PerformanceOverlay({ enabled, defaultOpen, }: PerformanceOverlayProps): import("react").JSX.Element | null;
1
+ import type { OverlayOptions } from "./vanilla.js";
2
+ export type PerformanceOverlayProps = OverlayOptions;
3
+ /** React adapter for the framework-independent overlay. */
4
+ export declare function PerformanceOverlay({ enabled, defaultOpen, thresholds }: PerformanceOverlayProps): null;
@@ -1,53 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
- export function PerformanceOverlay({ enabled = false, defaultOpen = false, }) {
4
- const [open, setOpen] = useState(defaultOpen);
5
- const [sample, setSample] = useState(null);
1
+ import { useEffect, useRef } from "react";
2
+ import { mountPerformanceOverlay } from "./vanilla.js";
3
+ /** React adapter for the framework-independent overlay. */
4
+ export function PerformanceOverlay({ enabled = false, defaultOpen = false, thresholds }) {
5
+ const handle = useRef(null);
6
6
  useEffect(() => {
7
- if (!enabled || !open)
8
- return;
9
- let frames = 0;
10
- let frameId = 0;
11
- let last = performance.now();
12
- let longTasks = 0;
13
- const tick = () => {
14
- frames += 1;
15
- frameId = requestAnimationFrame(tick);
16
- };
17
- frameId = requestAnimationFrame(tick);
18
- let observer;
19
- if (typeof PerformanceObserver !== "undefined" &&
20
- PerformanceObserver.supportedEntryTypes?.includes("longtask")) {
21
- observer = new PerformanceObserver((list) => {
22
- longTasks += list.getEntries().length;
23
- });
24
- observer.observe({ entryTypes: ["longtask"] });
25
- }
26
- const timer = window.setInterval(() => {
27
- const now = performance.now();
28
- 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
- });
35
- frames = 0;
36
- longTasks = 0;
37
- last = now;
38
- }, 1000);
39
- return () => {
40
- cancelAnimationFrame(frameId);
41
- clearInterval(timer);
42
- observer?.disconnect();
43
- };
44
- }, [enabled, open]);
45
- if (!enabled)
46
- return null;
47
- 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: [_jsxs("div", { children: [_jsx("span", { children: "Frame rate" }), _jsx("strong", { children: sample ? `${sample.fps} FPS` : "—" })] }), _jsxs("div", { children: [_jsx("span", { children: "JS heap" }), _jsx("strong", { children: sample?.heap ?? "—" })] }), _jsxs("div", { children: [_jsx("span", { children: "DOM elements" }), _jsx("strong", { children: sample?.nodes.toLocaleString() ?? "—" })] }), _jsxs("div", { children: [_jsx("span", { children: "Long tasks / sec" }), _jsx("strong", { children: sample
48
- ? typeof PerformanceObserver !== "undefined" &&
49
- PerformanceObserver.supportedEntryTypes?.includes("longtask")
50
- ? sample.longTasks
51
- : "Unavailable"
52
- : "—" })] }), _jsx("small", { children: "Heap and long tasks depend on browser support" })] }))] }));
7
+ const overlay = mountPerformanceOverlay({ defaultOpen, thresholds });
8
+ handle.current = overlay;
9
+ return () => { handle.current = null; overlay.destroy(); };
10
+ }, [defaultOpen, thresholds]);
11
+ useEffect(() => { handle.current?.setEnabled(enabled); }, [enabled, defaultOpen, thresholds]);
12
+ return null;
53
13
  }
package/dist/core.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export { assess, defaultThresholds } from "./thresholds.js";
2
+ export type { OverlayThresholds, Threshold, Status } from "./thresholds.js";
3
+ export { groupChildren, snapshotDom } from "./domTree.js";
4
+ export type { DomGroup, DomSnapshot } from "./domTree.js";
5
+ export type PerformanceSample = {
6
+ fps: number;
7
+ heapMb: number | null;
8
+ nodes: number;
9
+ longTasks: number | null;
10
+ maxTaskMs: number | null;
11
+ };
12
+ /** Starts sampling in the current document. Call the returned function to stop. */
13
+ export declare function observePerformance(onSample: (sample: PerformanceSample) => void): () => void;
package/dist/core.js ADDED
@@ -0,0 +1,30 @@
1
+ export { assess, defaultThresholds } from "./thresholds.js";
2
+ export { groupChildren, snapshotDom } from "./domTree.js";
3
+ /** Starts sampling in the current document. Call the returned function to stop. */
4
+ export function observePerformance(onSample) {
5
+ let frames = 0;
6
+ let last = performance.now();
7
+ let longTasks = 0;
8
+ let maxTaskMs = 0;
9
+ let frameId = 0;
10
+ const tick = () => { frames++; frameId = requestAnimationFrame(tick); };
11
+ frameId = requestAnimationFrame(tick);
12
+ const supported = typeof PerformanceObserver !== "undefined" && PerformanceObserver.supportedEntryTypes?.includes("longtask");
13
+ const observer = supported ? new PerformanceObserver((list) => {
14
+ for (const entry of list.getEntries()) {
15
+ longTasks++;
16
+ maxTaskMs = Math.max(maxTaskMs, entry.duration);
17
+ }
18
+ }) : undefined;
19
+ observer?.observe({ entryTypes: ["longtask"] });
20
+ const timer = window.setInterval(() => {
21
+ const now = performance.now();
22
+ const memory = performance.memory;
23
+ onSample({ 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 });
24
+ frames = 0;
25
+ longTasks = 0;
26
+ maxTaskMs = 0;
27
+ last = now;
28
+ }, 1000);
29
+ return () => { cancelAnimationFrame(frameId); clearInterval(timer); observer?.disconnect(); };
30
+ }
@@ -0,0 +1,12 @@
1
+ export type DomGroup = {
2
+ label: string;
3
+ elements: Element[];
4
+ count: number;
5
+ parentCount: number;
6
+ };
7
+ export type DomSnapshot = {
8
+ counts: Map<Element, number>;
9
+ roots: DomGroup[];
10
+ };
11
+ export declare function groupChildren(elements: Element[], counts: Map<Element, number>, parentCount: number): DomGroup[];
12
+ export declare function snapshotDom(body: Element): DomSnapshot;
@@ -0,0 +1,36 @@
1
+ function label(element) {
2
+ const id = element.id ? `#${element.id}` : "";
3
+ const role = element.getAttribute("role");
4
+ return `${element.tagName.toLowerCase()}${id}${role ? ` [${role}]` : ""}`;
5
+ }
6
+ export function groupChildren(elements, counts, parentCount) {
7
+ const groups = new Map();
8
+ for (const parent of elements) {
9
+ for (const child of Array.from(parent.children)) {
10
+ const count = counts.get(child);
11
+ if (!count)
12
+ continue;
13
+ // Group siblings by tag and role; IDs remain separate to preserve identifiable targets.
14
+ const name = label(child);
15
+ const group = groups.get(name) ?? { label: name, elements: [], count: 0, parentCount };
16
+ group.elements.push(child);
17
+ group.count += count;
18
+ groups.set(name, group);
19
+ }
20
+ }
21
+ return Array.from(groups.values()).sort((a, b) => b.count - a.count);
22
+ }
23
+ export function snapshotDom(body) {
24
+ const all = Array.from(body.querySelectorAll("*"));
25
+ const counts = new Map();
26
+ for (let index = all.length - 1; index >= 0; index--) {
27
+ const element = all[index];
28
+ if (element.closest("[data-performance-overlay]"))
29
+ continue;
30
+ const count = (counts.get(element) ?? 0) + 1;
31
+ counts.set(element, count);
32
+ if (element.parentElement)
33
+ counts.set(element.parentElement, (counts.get(element.parentElement) ?? 0) + count);
34
+ }
35
+ return { counts, roots: groupChildren([body], counts, counts.get(body) ?? 0) };
36
+ }
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
@@ -1 +1,2 @@
1
1
  export { PerformanceOverlay } from "./PerformanceOverlay.js";
2
+ export { defaultThresholds } from "./thresholds.js";
package/dist/style.css CHANGED
@@ -45,3 +45,22 @@
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; }
57
+ .dev-performance__body .dev-performance__inspector { display: block; padding: 0; }
58
+ .dev-performance__inspector button { display: block; width: 100%; margin-top: 5px; padding: 6px 8px; border: 1px solid #718297; border-radius: 4px; color: #f5f7fa; background: #26394a; font: inherit; text-align: left; overflow-wrap: anywhere; cursor: pointer; }
59
+ .dev-performance__inspector button:hover, .dev-performance__inspector button[aria-pressed="true"] { background: #38546b; }
60
+ .dev-performance__inspector button:focus-visible { outline: 2px solid #8bc6ff; outline-offset: 2px; }
61
+ .dev-performance__highlight { position: fixed; z-index: 2147483646; box-sizing: border-box; border: 3px solid #ffbd45; background: #ffbd4533; pointer-events: none; }
62
+ .dev-performance__body .dev-performance__path { display: flex; flex-wrap: wrap; justify-content: flex-start; gap: 4px; padding: 4px 0; }
63
+ .dev-performance__path button { display: inline-block; width: auto; margin: 0; }
64
+ .dev-performance__body .dev-performance__group { display: block; border-top: 1px solid #536170; padding: 8px 0; overflow-wrap: anywhere; }
65
+ .dev-performance__body .dev-performance__actions { display: flex; justify-content: flex-start; gap: 6px; padding: 0; }
66
+ .dev-performance__actions button { width: auto; }
@@ -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
+ }
@@ -0,0 +1,12 @@
1
+ import type { OverlayThresholds } from "./core.js";
2
+ export type OverlayOptions = {
3
+ enabled?: boolean;
4
+ defaultOpen?: boolean;
5
+ thresholds?: Partial<OverlayThresholds>;
6
+ };
7
+ export type OverlayHandle = {
8
+ setEnabled(enabled: boolean): void;
9
+ destroy(): void;
10
+ };
11
+ /** Mount an overlay in any browser app. Import the package stylesheet separately. */
12
+ export declare function mountPerformanceOverlay(options?: OverlayOptions): OverlayHandle;
@@ -0,0 +1,166 @@
1
+ import { assess, defaultThresholds, groupChildren, observePerformance, snapshotDom } from "./core.js";
2
+ /** Mount an overlay in any browser app. Import the package stylesheet separately. */
3
+ export function mountPerformanceOverlay(options = {}) {
4
+ let enabled = options.enabled ?? false;
5
+ let open = options.defaultOpen ?? false;
6
+ let destroyed = false;
7
+ let stopSampling;
8
+ let sample = null;
9
+ let snapshot = null;
10
+ let path = [];
11
+ let highlighted = null;
12
+ let boxes = [];
13
+ const limits = { ...defaultThresholds, ...options.thresholds };
14
+ const root = document.createElement("aside");
15
+ root.className = "dev-performance";
16
+ root.setAttribute("data-performance-overlay", "");
17
+ root.setAttribute("aria-label", "Development performance monitor");
18
+ const toggle = document.createElement("button");
19
+ toggle.type = "button";
20
+ toggle.className = "dev-performance__toggle";
21
+ const body = document.createElement("div");
22
+ body.className = "dev-performance__body";
23
+ const metrics = document.createElement("div");
24
+ const inspector = document.createElement("div");
25
+ inspector.className = "dev-performance__inspector";
26
+ const note = document.createElement("small");
27
+ note.textContent = "Indicative thresholds, not CPU/GPU usage. FPS varies by device; heap and task support vary by browser.";
28
+ body.append(metrics, inspector, note);
29
+ root.append(toggle, body);
30
+ const button = (text, action) => {
31
+ const el = document.createElement("button");
32
+ el.type = "button";
33
+ el.textContent = text;
34
+ el.addEventListener("click", action);
35
+ return el;
36
+ };
37
+ const clearHighlights = () => { boxes.forEach((box) => box.remove()); boxes = []; highlighted = null; };
38
+ const updateHighlights = () => {
39
+ boxes.forEach((box) => box.remove());
40
+ boxes = [];
41
+ if (!highlighted)
42
+ return;
43
+ for (const element of highlighted.elements.filter((item) => item.isConnected).slice(0, 50)) {
44
+ const rect = element.getBoundingClientRect();
45
+ if (!rect.width || !rect.height)
46
+ continue;
47
+ const box = document.createElement("div");
48
+ box.className = "dev-performance__highlight";
49
+ box.setAttribute("data-performance-overlay", "");
50
+ box.setAttribute("aria-hidden", "true");
51
+ Object.assign(box.style, { top: `${rect.top}px`, left: `${rect.left}px`, width: `${rect.width}px`, height: `${rect.height}px` });
52
+ document.body.append(box);
53
+ boxes.push(box);
54
+ }
55
+ };
56
+ const renderInspector = () => {
57
+ inspector.replaceChildren(button(snapshot ? "Refresh DOM scan" : "Explore DOM tree", () => { clearHighlights(); path = []; snapshot = snapshotDom(document.body); renderInspector(); }));
58
+ if (!snapshot)
59
+ return;
60
+ const info = document.createElement("p");
61
+ info.textContent = "Choose a group to explore its children. Counts include each element and its descendants. Large groups may not be slow.";
62
+ inspector.append(info);
63
+ const crumbs = document.createElement("div");
64
+ crumbs.className = "dev-performance__path";
65
+ crumbs.setAttribute("aria-label", "DOM path");
66
+ crumbs.append(button("document body", () => { path = []; clearHighlights(); renderInspector(); }));
67
+ path.forEach((group, index) => crumbs.append(button(`› ${group.label}${group.elements.length > 1 ? ` × ${group.elements.length}` : ""}`, () => { path = path.slice(0, index + 1); clearHighlights(); renderInspector(); })));
68
+ inspector.append(crumbs);
69
+ const current = path[path.length - 1];
70
+ const groups = current ? groupChildren(current.elements, snapshot.counts, current.count) : snapshot.roots;
71
+ if (current) {
72
+ const count = document.createElement("p");
73
+ count.textContent = `${current.count.toLocaleString()} elements · ${current.elements.length.toLocaleString()} matching nodes`;
74
+ inspector.append(count);
75
+ }
76
+ if (!groups.length) {
77
+ const empty = document.createElement("p");
78
+ empty.textContent = "No child elements in this snapshot.";
79
+ inspector.append(empty);
80
+ }
81
+ for (const group of groups) {
82
+ const row = document.createElement("div");
83
+ row.className = "dev-performance__group";
84
+ const label = document.createElement("strong");
85
+ label.textContent = `${group.label}${group.elements.length > 1 ? ` × ${group.elements.length}` : ""}`;
86
+ const count = document.createElement("span");
87
+ count.textContent = `${group.count.toLocaleString()} elements · ${group.parentCount ? Math.round(group.count / group.parentCount * 100) : 0}% of parent`;
88
+ const actions = document.createElement("div");
89
+ actions.className = "dev-performance__actions";
90
+ actions.append(button("Highlight", () => { clearHighlights(); highlighted = group; const first = group.elements.find((item) => item.isConnected && item.getBoundingClientRect().width > 0 && item.getBoundingClientRect().height > 0); first?.scrollIntoView({ block: "nearest", inline: "nearest" }); updateHighlights(); renderInspector(); }), button("Explore", () => { clearHighlights(); path.push(group); renderInspector(); }));
91
+ row.append(label, document.createElement("br"), count, actions);
92
+ inspector.append(row);
93
+ }
94
+ if (highlighted) {
95
+ const msg = document.createElement("p");
96
+ msg.textContent = "Highlighting up to 50 connected elements. ";
97
+ msg.append(button("Clear highlight", () => { clearHighlights(); renderInspector(); }));
98
+ inspector.append(msg);
99
+ }
100
+ };
101
+ const renderMetrics = () => {
102
+ const rows = [
103
+ { label: "Frame rate", value: sample ? `${sample.fps} FPS` : "—", status: assess(sample?.fps ?? null, limits.fps, true), advice: "Check Performance recording for slow frames, rendering and animations. Compare with this device’s refresh rate." },
104
+ { label: "JS heap", value: sample?.heapMb != null ? `${sample.heapMb.toFixed(0)} MB` : "Unavailable", status: assess(sample?.heapMb ?? null, limits.heapMb), advice: "Compare heap snapshots after repeating the same action; inspect retained objects and detached DOM nodes." },
105
+ { label: "DOM elements", value: sample ? sample.nodes.toLocaleString() : "—", status: assess(sample?.nodes ?? null, limits.nodes), advice: "Inspect large lists, tables and hidden content; consider pagination or virtualization." },
106
+ { label: "Longest task / sec", value: sample?.maxTaskMs != null ? `${sample.maxTaskMs.toFixed(0)} ms (${sample.longTasks})` : "Unavailable", status: assess(sample?.maxTaskMs ?? null, limits.longTaskMs), advice: "Record Performance and inspect the longest main-thread tasks and scripts." },
107
+ ];
108
+ rows.forEach((row, index) => {
109
+ let item = metrics.children.item(index);
110
+ if (!item) {
111
+ item = document.createElement("div");
112
+ item.className = "dev-performance__metric";
113
+ const line = document.createElement("div");
114
+ line.className = "dev-performance__line";
115
+ line.append(document.createElement("span"), document.createElement("strong"));
116
+ const status = document.createElement("span");
117
+ const advice = document.createElement("p");
118
+ advice.className = "dev-performance__advice";
119
+ item.append(line, status, advice);
120
+ if (index === 2)
121
+ item.append(inspector);
122
+ metrics.append(item);
123
+ }
124
+ const line = item.children[0];
125
+ line.children[0].textContent = row.label;
126
+ line.children[1].textContent = row.value;
127
+ const status = item.children[1];
128
+ status.className = `dev-performance__status dev-performance__status--${row.status}`;
129
+ status.textContent = row.status === "ok" ? "OK" : row.status === "warning" ? "Watch" : row.status === "critical" ? "Investigate" : "Unavailable";
130
+ const advice = item.children[2];
131
+ advice.textContent = row.advice;
132
+ advice.hidden = row.status !== "warning" && row.status !== "critical";
133
+ });
134
+ };
135
+ const sync = () => {
136
+ stopSampling?.();
137
+ stopSampling = undefined;
138
+ if (!enabled || !open || document.hidden) {
139
+ sample = null;
140
+ clearHighlights();
141
+ }
142
+ if (enabled && open && !document.hidden)
143
+ stopSampling = observePerformance((next) => { sample = next; renderMetrics(); });
144
+ root.hidden = !enabled;
145
+ toggle.textContent = open ? "Hide performance" : "Performance";
146
+ toggle.setAttribute("aria-expanded", String(open));
147
+ body.hidden = !open;
148
+ renderMetrics();
149
+ };
150
+ toggle.addEventListener("click", () => { open = !open; sync(); });
151
+ const visibility = () => sync();
152
+ document.addEventListener("visibilitychange", visibility);
153
+ window.addEventListener("scroll", updateHighlights, true);
154
+ window.addEventListener("resize", updateHighlights);
155
+ document.body.append(root);
156
+ renderInspector();
157
+ sync();
158
+ return {
159
+ setEnabled(value) { if (!destroyed && enabled !== value) {
160
+ enabled = value;
161
+ sync();
162
+ } },
163
+ destroy() { if (destroyed)
164
+ return; destroyed = true; stopSampling?.(); clearHighlights(); document.removeEventListener("visibilitychange", visibility); window.removeEventListener("scroll", updateHighlights, true); window.removeEventListener("resize", updateHighlights); root.remove(); },
165
+ };
166
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@piyawat37/performance-overlay",
3
- "version": "0.1.0",
4
- "description": "A configurable client-side performance overlay for React applications",
3
+ "version": "0.1.2",
4
+ "description": "A framework-independent browser performance overlay with a React adapter",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -12,7 +12,15 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "import": "./dist/index.js"
14
14
  },
15
- "./style.css": "./dist/style.css"
15
+ "./style.css": "./dist/style.css",
16
+ "./vanilla": {
17
+ "types": "./dist/vanilla.d.ts",
18
+ "import": "./dist/vanilla.js"
19
+ },
20
+ "./core": {
21
+ "types": "./dist/core.d.ts",
22
+ "import": "./dist/core.js"
23
+ }
16
24
  },
17
25
  "sideEffects": [
18
26
  "**/*.css"
@@ -22,10 +30,23 @@
22
30
  "prepare": "npm run build"
23
31
  },
24
32
  "peerDependencies": {
25
- "react": ">=18 <20"
33
+ "react": ">=18 <20",
34
+ "react-dom": ">=18 <20"
26
35
  },
27
36
  "devDependencies": {
28
37
  "@types/react": "^19.0.0",
38
+ "@types/react-dom": "^19.0.0",
39
+ "jsdom": "^31.0.0",
40
+ "react": "^19.3.1",
41
+ "react-dom": "^19.3.1",
29
42
  "typescript": "^5.9.3"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "react": {
46
+ "optional": true
47
+ },
48
+ "react-dom": {
49
+ "optional": true
50
+ }
30
51
  }
31
52
  }