@react-perfscope/ui 0.7.0 → 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 +139 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +119 -26
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.cjs
CHANGED
|
@@ -90,7 +90,8 @@ function mountShadow(vnode, opts = {}) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// src/app.tsx
|
|
93
|
-
var
|
|
93
|
+
var import_preact4 = require("preact");
|
|
94
|
+
var import_hooks6 = require("preact/hooks");
|
|
94
95
|
|
|
95
96
|
// src/i18n.ts
|
|
96
97
|
var import_preact2 = require("preact");
|
|
@@ -288,8 +289,8 @@ function isLang(v) {
|
|
|
288
289
|
return v === "en" || v === "ko";
|
|
289
290
|
}
|
|
290
291
|
function readStoredLang() {
|
|
291
|
-
if (typeof localStorage === "undefined") return "en";
|
|
292
292
|
try {
|
|
293
|
+
if (typeof localStorage === "undefined") return "en";
|
|
293
294
|
const v = localStorage.getItem(STORAGE_KEY);
|
|
294
295
|
return isLang(v) ? v : "en";
|
|
295
296
|
} catch {
|
|
@@ -306,11 +307,11 @@ function I18nProvider({ children }) {
|
|
|
306
307
|
const [lang, setLangState] = (0, import_hooks.useState)(() => readStoredLang());
|
|
307
308
|
const setLang = (0, import_hooks.useCallback)((l) => {
|
|
308
309
|
setLangState(l);
|
|
309
|
-
|
|
310
|
-
|
|
310
|
+
try {
|
|
311
|
+
if (typeof localStorage !== "undefined") {
|
|
311
312
|
localStorage.setItem(STORAGE_KEY, l);
|
|
312
|
-
} catch {
|
|
313
313
|
}
|
|
314
|
+
} catch {
|
|
314
315
|
}
|
|
315
316
|
}, []);
|
|
316
317
|
const value = (0, import_hooks.useMemo)(() => ({ lang, setLang, t: STRINGS[lang] }), [lang, setLang]);
|
|
@@ -390,7 +391,7 @@ function Widget(props) {
|
|
|
390
391
|
}
|
|
391
392
|
|
|
392
393
|
// src/panel.tsx
|
|
393
|
-
var
|
|
394
|
+
var import_hooks5 = require("preact/hooks");
|
|
394
395
|
var import_core4 = require("@react-perfscope/core");
|
|
395
396
|
|
|
396
397
|
// src/overlay.ts
|
|
@@ -443,12 +444,12 @@ function showArrow(id, from, to, color = DEFAULT_BORDER) {
|
|
|
443
444
|
const minX = Math.min(from.x, to.x);
|
|
444
445
|
const minY = Math.min(from.y, to.y);
|
|
445
446
|
const w = Math.max(2, Math.abs(to.x - from.x));
|
|
446
|
-
const
|
|
447
|
+
const h4 = Math.max(2, Math.abs(to.y - from.y));
|
|
447
448
|
svg.style.left = `${minX}px`;
|
|
448
449
|
svg.style.top = `${minY}px`;
|
|
449
450
|
svg.setAttribute("width", `${w}`);
|
|
450
|
-
svg.setAttribute("height", `${
|
|
451
|
-
svg.setAttribute("viewBox", `0 0 ${w} ${
|
|
451
|
+
svg.setAttribute("height", `${h4}`);
|
|
452
|
+
svg.setAttribute("viewBox", `0 0 ${w} ${h4}`);
|
|
452
453
|
svg.style.opacity = "1";
|
|
453
454
|
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
|
454
455
|
const fx = from.x - minX;
|
|
@@ -530,7 +531,7 @@ var WEB_VITAL_THRESHOLDS = {
|
|
|
530
531
|
TTFB: [800, 1800]
|
|
531
532
|
};
|
|
532
533
|
function webVitalRating(name, value) {
|
|
533
|
-
const [good, needs] = WEB_VITAL_THRESHOLDS[name];
|
|
534
|
+
const [good, needs] = WEB_VITAL_THRESHOLDS[name] ?? [Infinity, Infinity];
|
|
534
535
|
if (value <= good) return "good";
|
|
535
536
|
if (value <= needs) return "needs";
|
|
536
537
|
return "poor";
|
|
@@ -591,15 +592,15 @@ var WEB_VITAL_UNIT = {
|
|
|
591
592
|
TTFB: "ms"
|
|
592
593
|
};
|
|
593
594
|
function formatVitalValue(name, value) {
|
|
594
|
-
|
|
595
|
+
const unit = WEB_VITAL_UNIT[name] ?? "ms";
|
|
596
|
+
if (unit !== "ms") return value.toFixed(3);
|
|
595
597
|
if (value >= 1e3) return (value / 1e3).toFixed(2) + "s";
|
|
596
|
-
return value.toFixed(0);
|
|
598
|
+
return value.toFixed(0) + "ms";
|
|
597
599
|
}
|
|
598
600
|
function VitalChip({ s }) {
|
|
599
601
|
const { t } = useI18n();
|
|
600
602
|
const rating = webVitalRating(s.name, s.value);
|
|
601
603
|
const color = RATING_COLOR[rating];
|
|
602
|
-
const unit = WEB_VITAL_UNIT[s.name];
|
|
603
604
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
604
605
|
"span",
|
|
605
606
|
{
|
|
@@ -631,10 +632,7 @@ function VitalChip({ s }) {
|
|
|
631
632
|
}
|
|
632
633
|
),
|
|
633
634
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { style: { color: "#e6e6e6" }, children: s.name }),
|
|
634
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
635
|
-
formatVitalValue(s.name, s.value),
|
|
636
|
-
unit !== "s" ? unit : ""
|
|
637
|
-
] })
|
|
635
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color }, children: formatVitalValue(s.name, s.value) })
|
|
638
636
|
]
|
|
639
637
|
}
|
|
640
638
|
);
|
|
@@ -1936,6 +1934,7 @@ function RenderInsights({ signals, onSelect }) {
|
|
|
1936
1934
|
}
|
|
1937
1935
|
|
|
1938
1936
|
// src/inp-episode.tsx
|
|
1937
|
+
var import_hooks3 = require("preact/hooks");
|
|
1939
1938
|
var import_core2 = require("@react-perfscope/core");
|
|
1940
1939
|
|
|
1941
1940
|
// src/episode-shared.tsx
|
|
@@ -2006,7 +2005,7 @@ function phaseMs(episode, phase) {
|
|
|
2006
2005
|
}
|
|
2007
2006
|
function InpEpisode({ signals }) {
|
|
2008
2007
|
const { t } = useI18n();
|
|
2009
|
-
const episode = worstInteractionEpisode(signals);
|
|
2008
|
+
const episode = (0, import_hooks3.useMemo)(() => worstInteractionEpisode(signals), [signals]);
|
|
2010
2009
|
if (!episode || episode.anchor.kind !== "interaction") return null;
|
|
2011
2010
|
const a = episode.anchor;
|
|
2012
2011
|
const target = a.target ? ` on ${a.target}` : "";
|
|
@@ -2062,6 +2061,7 @@ function InpEpisode({ signals }) {
|
|
|
2062
2061
|
}
|
|
2063
2062
|
|
|
2064
2063
|
// src/long-task-episode.tsx
|
|
2064
|
+
var import_hooks4 = require("preact/hooks");
|
|
2065
2065
|
var import_core3 = require("@react-perfscope/core");
|
|
2066
2066
|
var import_jsx_runtime8 = require("preact/jsx-runtime");
|
|
2067
2067
|
function worstLongTaskEpisode(signals) {
|
|
@@ -2074,7 +2074,7 @@ function worstLongTaskEpisode(signals) {
|
|
|
2074
2074
|
}
|
|
2075
2075
|
function LongTaskEpisode({ signals }) {
|
|
2076
2076
|
const { t } = useI18n();
|
|
2077
|
-
const episode = worstLongTaskEpisode(signals);
|
|
2077
|
+
const episode = (0, import_hooks4.useMemo)(() => worstLongTaskEpisode(signals), [signals]);
|
|
2078
2078
|
if (!episode || episode.anchor.kind !== "long-task") return null;
|
|
2079
2079
|
if (episode.members.length === 0) return null;
|
|
2080
2080
|
const a = episode.anchor;
|
|
@@ -2413,9 +2413,9 @@ function StackFrames({
|
|
|
2413
2413
|
}) {
|
|
2414
2414
|
const { t } = useI18n();
|
|
2415
2415
|
const original = raw.slice(0, limit);
|
|
2416
|
-
const [frames, setFrames] = (0,
|
|
2417
|
-
const [resolving, setResolving] = (0,
|
|
2418
|
-
(0,
|
|
2416
|
+
const [frames, setFrames] = (0, import_hooks5.useState)(original);
|
|
2417
|
+
const [resolving, setResolving] = (0, import_hooks5.useState)(false);
|
|
2418
|
+
(0, import_hooks5.useEffect)(() => {
|
|
2419
2419
|
if (!resolveFrame || original.length === 0) return;
|
|
2420
2420
|
let cancelled = false;
|
|
2421
2421
|
setResolving(true);
|
|
@@ -2454,8 +2454,8 @@ function HotFunctions({
|
|
|
2454
2454
|
resolveFrame
|
|
2455
2455
|
}) {
|
|
2456
2456
|
const { t } = useI18n();
|
|
2457
|
-
const [frames, setFrames] = (0,
|
|
2458
|
-
(0,
|
|
2457
|
+
const [frames, setFrames] = (0, import_hooks5.useState)(attribution.map((a) => a.frame));
|
|
2458
|
+
(0, import_hooks5.useEffect)(() => {
|
|
2459
2459
|
if (!resolveFrame) return;
|
|
2460
2460
|
let cancelled = false;
|
|
2461
2461
|
Promise.all(attribution.map((a) => resolveFrame(a.frame))).then((resolved) => {
|
|
@@ -2899,21 +2899,21 @@ function LanguageToggle() {
|
|
|
2899
2899
|
function Panel(props) {
|
|
2900
2900
|
const { result, onClose, position = "bottom-right", resolveFrame } = props;
|
|
2901
2901
|
const { t } = useI18n();
|
|
2902
|
-
const grouped = (0,
|
|
2902
|
+
const grouped = (0, import_hooks5.useMemo)(() => groupByKind(result.signals), [result.signals]);
|
|
2903
2903
|
const kindsPresent = KIND_ORDER.filter((k) => grouped[k].length > 0);
|
|
2904
|
-
const unsupported = (0,
|
|
2904
|
+
const unsupported = (0, import_hooks5.useMemo)(() => (0, import_core4.unsupportedKinds)(), []);
|
|
2905
2905
|
const hasTimelineSignals = result.signals.some((s) => s.kind !== "web-vital") || (result.heapSamples?.length ?? 0) > 0 || (result.frames?.length ?? 0) > 0 || (result.leakSuspects?.length ?? 0) > 0;
|
|
2906
|
-
const [activeTab, setActiveTab] = (0,
|
|
2906
|
+
const [activeTab, setActiveTab] = (0, import_hooks5.useState)(
|
|
2907
2907
|
kindsPresent[0] ?? "forced-reflow"
|
|
2908
2908
|
);
|
|
2909
2909
|
const activeKind = activeTab === "timeline" ? null : activeTab;
|
|
2910
2910
|
const setActiveKind = (k) => setActiveTab(k);
|
|
2911
|
-
const [expandedKey, setExpandedKey] = (0,
|
|
2912
|
-
const [groupMode, setGroupMode] = (0,
|
|
2913
|
-
const [sortMode, setSortMode] = (0,
|
|
2914
|
-
const [filterText, setFilterText] = (0,
|
|
2915
|
-
const activeOverlayCount = (0,
|
|
2916
|
-
(0,
|
|
2911
|
+
const [expandedKey, setExpandedKey] = (0, import_hooks5.useState)(null);
|
|
2912
|
+
const [groupMode, setGroupMode] = (0, import_hooks5.useState)({});
|
|
2913
|
+
const [sortMode, setSortMode] = (0, import_hooks5.useState)({});
|
|
2914
|
+
const [filterText, setFilterText] = (0, import_hooks5.useState)({});
|
|
2915
|
+
const activeOverlayCount = (0, import_hooks5.useRef)(0);
|
|
2916
|
+
(0, import_hooks5.useEffect)(() => () => hideAllOverlays(), []);
|
|
2917
2917
|
function handleHover(signal) {
|
|
2918
2918
|
if (!signal) {
|
|
2919
2919
|
for (let i = 0; i < activeOverlayCount.current; i++) {
|
|
@@ -3282,21 +3282,81 @@ function Panel(props) {
|
|
|
3282
3282
|
|
|
3283
3283
|
// src/app.tsx
|
|
3284
3284
|
var import_jsx_runtime10 = require("preact/jsx-runtime");
|
|
3285
|
+
var PanelErrorBoundary = class extends import_preact4.Component {
|
|
3286
|
+
constructor() {
|
|
3287
|
+
super(...arguments);
|
|
3288
|
+
this.state = { error: null };
|
|
3289
|
+
}
|
|
3290
|
+
static getDerivedStateFromError(error) {
|
|
3291
|
+
return { error };
|
|
3292
|
+
}
|
|
3293
|
+
componentDidCatch(error) {
|
|
3294
|
+
console.warn("[react-perfscope] panel crashed:", error);
|
|
3295
|
+
}
|
|
3296
|
+
render() {
|
|
3297
|
+
if (this.state.error === null) return this.props.children;
|
|
3298
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
|
|
3299
|
+
"div",
|
|
3300
|
+
{
|
|
3301
|
+
role: "alert",
|
|
3302
|
+
style: {
|
|
3303
|
+
position: "fixed",
|
|
3304
|
+
bottom: "16px",
|
|
3305
|
+
right: "16px",
|
|
3306
|
+
zIndex: 2147483647,
|
|
3307
|
+
display: "flex",
|
|
3308
|
+
alignItems: "center",
|
|
3309
|
+
gap: "8px",
|
|
3310
|
+
padding: "8px 12px",
|
|
3311
|
+
background: "#141414",
|
|
3312
|
+
border: "1px solid #ff3b30",
|
|
3313
|
+
borderRadius: "8px",
|
|
3314
|
+
color: "#e6e6e6",
|
|
3315
|
+
fontSize: "12px",
|
|
3316
|
+
fontFamily: "SF Mono, Menlo, Consolas, monospace"
|
|
3317
|
+
},
|
|
3318
|
+
children: [
|
|
3319
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: "react-perfscope panel crashed \u2014 see console" }),
|
|
3320
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
3321
|
+
"button",
|
|
3322
|
+
{
|
|
3323
|
+
"aria-label": "Close panel",
|
|
3324
|
+
onClick: () => {
|
|
3325
|
+
this.setState({ error: null });
|
|
3326
|
+
this.props.onClose();
|
|
3327
|
+
},
|
|
3328
|
+
style: {
|
|
3329
|
+
background: "none",
|
|
3330
|
+
border: "1px solid #444",
|
|
3331
|
+
borderRadius: "4px",
|
|
3332
|
+
color: "#e6e6e6",
|
|
3333
|
+
cursor: "pointer",
|
|
3334
|
+
padding: "2px 8px"
|
|
3335
|
+
},
|
|
3336
|
+
children: "\xD7"
|
|
3337
|
+
}
|
|
3338
|
+
)
|
|
3339
|
+
]
|
|
3340
|
+
}
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3285
3344
|
function App(props) {
|
|
3286
3345
|
const { recorder, position = "bottom-right", resolveFrame, finalize } = props;
|
|
3287
|
-
const [recording, setRecording] = (0,
|
|
3288
|
-
const [result, setResult] = (0,
|
|
3289
|
-
const [elapsedMs, setElapsedMs] = (0,
|
|
3290
|
-
const startedAtRef = (0,
|
|
3291
|
-
const rafRef = (0,
|
|
3292
|
-
const resultTokenRef = (0,
|
|
3293
|
-
(0,
|
|
3346
|
+
const [recording, setRecording] = (0, import_hooks6.useState)(false);
|
|
3347
|
+
const [result, setResult] = (0, import_hooks6.useState)(null);
|
|
3348
|
+
const [elapsedMs, setElapsedMs] = (0, import_hooks6.useState)(0);
|
|
3349
|
+
const startedAtRef = (0, import_hooks6.useRef)(0);
|
|
3350
|
+
const rafRef = (0, import_hooks6.useRef)(null);
|
|
3351
|
+
const resultTokenRef = (0, import_hooks6.useRef)(0);
|
|
3352
|
+
(0, import_hooks6.useEffect)(() => {
|
|
3294
3353
|
if (!recording) {
|
|
3295
3354
|
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
3296
3355
|
return;
|
|
3297
3356
|
}
|
|
3298
3357
|
const tick = () => {
|
|
3299
|
-
|
|
3358
|
+
const elapsed = performance.now() - startedAtRef.current;
|
|
3359
|
+
setElapsedMs(Math.floor(elapsed / 1e3) * 1e3);
|
|
3300
3360
|
rafRef.current = requestAnimationFrame(tick);
|
|
3301
3361
|
};
|
|
3302
3362
|
rafRef.current = requestAnimationFrame(tick);
|
|
@@ -3309,12 +3369,23 @@ function App(props) {
|
|
|
3309
3369
|
startedAtRef.current = performance.now();
|
|
3310
3370
|
setElapsedMs(0);
|
|
3311
3371
|
setResult(null);
|
|
3312
|
-
|
|
3372
|
+
try {
|
|
3373
|
+
recorder.start();
|
|
3374
|
+
} catch (err) {
|
|
3375
|
+
console.warn("[react-perfscope] failed to start recording:", err);
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3313
3378
|
setRecording(true);
|
|
3314
3379
|
} else {
|
|
3315
|
-
const r = recorder.stop();
|
|
3316
|
-
const token = ++resultTokenRef.current;
|
|
3317
3380
|
setRecording(false);
|
|
3381
|
+
let r;
|
|
3382
|
+
try {
|
|
3383
|
+
r = recorder.stop();
|
|
3384
|
+
} catch (err) {
|
|
3385
|
+
console.warn("[react-perfscope] failed to stop recording:", err);
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
const token = ++resultTokenRef.current;
|
|
3318
3389
|
setResult(r);
|
|
3319
3390
|
if (finalize) {
|
|
3320
3391
|
finalize(r).then((enriched) => {
|
|
@@ -3337,14 +3408,36 @@ function App(props) {
|
|
|
3337
3408
|
position
|
|
3338
3409
|
}
|
|
3339
3410
|
),
|
|
3340
|
-
result !== null && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Panel, { result, position, onClose, resolveFrame })
|
|
3411
|
+
result !== null && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PanelErrorBoundary, { onClose, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Panel, { result, position, onClose, resolveFrame }) })
|
|
3341
3412
|
] });
|
|
3342
3413
|
}
|
|
3343
3414
|
|
|
3344
3415
|
// src/mount.tsx
|
|
3345
3416
|
var import_jsx_runtime11 = require("preact/jsx-runtime");
|
|
3346
3417
|
function mount(opts) {
|
|
3347
|
-
|
|
3418
|
+
if (!opts.host && !document.body) {
|
|
3419
|
+
let torn = false;
|
|
3420
|
+
let unmount = null;
|
|
3421
|
+
const onReady = () => {
|
|
3422
|
+
if (torn || !document.body) return;
|
|
3423
|
+
unmount = mountNow(opts, document.body);
|
|
3424
|
+
};
|
|
3425
|
+
document.addEventListener("DOMContentLoaded", onReady, { once: true });
|
|
3426
|
+
return () => {
|
|
3427
|
+
torn = true;
|
|
3428
|
+
document.removeEventListener("DOMContentLoaded", onReady);
|
|
3429
|
+
unmount?.();
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
return mountNow(opts, opts.host ?? document.body);
|
|
3433
|
+
}
|
|
3434
|
+
function mountNow(opts, host) {
|
|
3435
|
+
const { recorder, position = "bottom-right", resolveFrame, finalize } = opts;
|
|
3436
|
+
if (host.querySelector("[data-perfscope-host]")) {
|
|
3437
|
+
console.warn("[react-perfscope] mount(): a widget is already mounted here \u2014 ignoring this call");
|
|
3438
|
+
return () => {
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3348
3441
|
return mountShadow(
|
|
3349
3442
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(App, { recorder, position, resolveFrame, finalize }),
|
|
3350
3443
|
{ parent: host }
|