@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.js
CHANGED
|
@@ -57,6 +57,7 @@ function mountShadow(vnode, opts = {}) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// src/app.tsx
|
|
60
|
+
import { Component } from "preact";
|
|
60
61
|
import { useState as useState4, useRef as useRef3, useEffect as useEffect2 } from "preact/hooks";
|
|
61
62
|
|
|
62
63
|
// src/i18n.ts
|
|
@@ -255,8 +256,8 @@ function isLang(v) {
|
|
|
255
256
|
return v === "en" || v === "ko";
|
|
256
257
|
}
|
|
257
258
|
function readStoredLang() {
|
|
258
|
-
if (typeof localStorage === "undefined") return "en";
|
|
259
259
|
try {
|
|
260
|
+
if (typeof localStorage === "undefined") return "en";
|
|
260
261
|
const v = localStorage.getItem(STORAGE_KEY);
|
|
261
262
|
return isLang(v) ? v : "en";
|
|
262
263
|
} catch {
|
|
@@ -273,11 +274,11 @@ function I18nProvider({ children }) {
|
|
|
273
274
|
const [lang, setLangState] = useState(() => readStoredLang());
|
|
274
275
|
const setLang = useCallback((l) => {
|
|
275
276
|
setLangState(l);
|
|
276
|
-
|
|
277
|
-
|
|
277
|
+
try {
|
|
278
|
+
if (typeof localStorage !== "undefined") {
|
|
278
279
|
localStorage.setItem(STORAGE_KEY, l);
|
|
279
|
-
} catch {
|
|
280
280
|
}
|
|
281
|
+
} catch {
|
|
281
282
|
}
|
|
282
283
|
}, []);
|
|
283
284
|
const value = useMemo(() => ({ lang, setLang, t: STRINGS[lang] }), [lang, setLang]);
|
|
@@ -357,7 +358,7 @@ function Widget(props) {
|
|
|
357
358
|
}
|
|
358
359
|
|
|
359
360
|
// src/panel.tsx
|
|
360
|
-
import { useState as useState3, useMemo as
|
|
361
|
+
import { useState as useState3, useMemo as useMemo5, useEffect, useRef as useRef2 } from "preact/hooks";
|
|
361
362
|
import { unsupportedKinds } from "@react-perfscope/core";
|
|
362
363
|
|
|
363
364
|
// src/overlay.ts
|
|
@@ -410,12 +411,12 @@ function showArrow(id, from, to, color = DEFAULT_BORDER) {
|
|
|
410
411
|
const minX = Math.min(from.x, to.x);
|
|
411
412
|
const minY = Math.min(from.y, to.y);
|
|
412
413
|
const w = Math.max(2, Math.abs(to.x - from.x));
|
|
413
|
-
const
|
|
414
|
+
const h4 = Math.max(2, Math.abs(to.y - from.y));
|
|
414
415
|
svg.style.left = `${minX}px`;
|
|
415
416
|
svg.style.top = `${minY}px`;
|
|
416
417
|
svg.setAttribute("width", `${w}`);
|
|
417
|
-
svg.setAttribute("height", `${
|
|
418
|
-
svg.setAttribute("viewBox", `0 0 ${w} ${
|
|
418
|
+
svg.setAttribute("height", `${h4}`);
|
|
419
|
+
svg.setAttribute("viewBox", `0 0 ${w} ${h4}`);
|
|
419
420
|
svg.style.opacity = "1";
|
|
420
421
|
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
|
421
422
|
const fx = from.x - minX;
|
|
@@ -497,7 +498,7 @@ var WEB_VITAL_THRESHOLDS = {
|
|
|
497
498
|
TTFB: [800, 1800]
|
|
498
499
|
};
|
|
499
500
|
function webVitalRating(name, value) {
|
|
500
|
-
const [good, needs] = WEB_VITAL_THRESHOLDS[name];
|
|
501
|
+
const [good, needs] = WEB_VITAL_THRESHOLDS[name] ?? [Infinity, Infinity];
|
|
501
502
|
if (value <= good) return "good";
|
|
502
503
|
if (value <= needs) return "needs";
|
|
503
504
|
return "poor";
|
|
@@ -558,15 +559,15 @@ var WEB_VITAL_UNIT = {
|
|
|
558
559
|
TTFB: "ms"
|
|
559
560
|
};
|
|
560
561
|
function formatVitalValue(name, value) {
|
|
561
|
-
|
|
562
|
+
const unit = WEB_VITAL_UNIT[name] ?? "ms";
|
|
563
|
+
if (unit !== "ms") return value.toFixed(3);
|
|
562
564
|
if (value >= 1e3) return (value / 1e3).toFixed(2) + "s";
|
|
563
|
-
return value.toFixed(0);
|
|
565
|
+
return value.toFixed(0) + "ms";
|
|
564
566
|
}
|
|
565
567
|
function VitalChip({ s }) {
|
|
566
568
|
const { t } = useI18n();
|
|
567
569
|
const rating = webVitalRating(s.name, s.value);
|
|
568
570
|
const color = RATING_COLOR[rating];
|
|
569
|
-
const unit = WEB_VITAL_UNIT[s.name];
|
|
570
571
|
return /* @__PURE__ */ jsxs2(
|
|
571
572
|
"span",
|
|
572
573
|
{
|
|
@@ -598,10 +599,7 @@ function VitalChip({ s }) {
|
|
|
598
599
|
}
|
|
599
600
|
),
|
|
600
601
|
/* @__PURE__ */ jsx2("strong", { style: { color: "#e6e6e6" }, children: s.name }),
|
|
601
|
-
/* @__PURE__ */
|
|
602
|
-
formatVitalValue(s.name, s.value),
|
|
603
|
-
unit !== "s" ? unit : ""
|
|
604
|
-
] })
|
|
602
|
+
/* @__PURE__ */ jsx2("span", { style: { color }, children: formatVitalValue(s.name, s.value) })
|
|
605
603
|
]
|
|
606
604
|
}
|
|
607
605
|
);
|
|
@@ -1903,6 +1901,7 @@ function RenderInsights({ signals, onSelect }) {
|
|
|
1903
1901
|
}
|
|
1904
1902
|
|
|
1905
1903
|
// src/inp-episode.tsx
|
|
1904
|
+
import { useMemo as useMemo3 } from "preact/hooks";
|
|
1906
1905
|
import { correlate } from "@react-perfscope/core";
|
|
1907
1906
|
|
|
1908
1907
|
// src/episode-shared.tsx
|
|
@@ -1973,7 +1972,7 @@ function phaseMs(episode, phase) {
|
|
|
1973
1972
|
}
|
|
1974
1973
|
function InpEpisode({ signals }) {
|
|
1975
1974
|
const { t } = useI18n();
|
|
1976
|
-
const episode = worstInteractionEpisode(signals);
|
|
1975
|
+
const episode = useMemo3(() => worstInteractionEpisode(signals), [signals]);
|
|
1977
1976
|
if (!episode || episode.anchor.kind !== "interaction") return null;
|
|
1978
1977
|
const a = episode.anchor;
|
|
1979
1978
|
const target = a.target ? ` on ${a.target}` : "";
|
|
@@ -2029,6 +2028,7 @@ function InpEpisode({ signals }) {
|
|
|
2029
2028
|
}
|
|
2030
2029
|
|
|
2031
2030
|
// src/long-task-episode.tsx
|
|
2031
|
+
import { useMemo as useMemo4 } from "preact/hooks";
|
|
2032
2032
|
import { correlate as correlate2 } from "@react-perfscope/core";
|
|
2033
2033
|
import { jsx as jsx8, jsxs as jsxs8 } from "preact/jsx-runtime";
|
|
2034
2034
|
function worstLongTaskEpisode(signals) {
|
|
@@ -2041,7 +2041,7 @@ function worstLongTaskEpisode(signals) {
|
|
|
2041
2041
|
}
|
|
2042
2042
|
function LongTaskEpisode({ signals }) {
|
|
2043
2043
|
const { t } = useI18n();
|
|
2044
|
-
const episode = worstLongTaskEpisode(signals);
|
|
2044
|
+
const episode = useMemo4(() => worstLongTaskEpisode(signals), [signals]);
|
|
2045
2045
|
if (!episode || episode.anchor.kind !== "long-task") return null;
|
|
2046
2046
|
if (episode.members.length === 0) return null;
|
|
2047
2047
|
const a = episode.anchor;
|
|
@@ -2866,9 +2866,9 @@ function LanguageToggle() {
|
|
|
2866
2866
|
function Panel(props) {
|
|
2867
2867
|
const { result, onClose, position = "bottom-right", resolveFrame } = props;
|
|
2868
2868
|
const { t } = useI18n();
|
|
2869
|
-
const grouped =
|
|
2869
|
+
const grouped = useMemo5(() => groupByKind(result.signals), [result.signals]);
|
|
2870
2870
|
const kindsPresent = KIND_ORDER.filter((k) => grouped[k].length > 0);
|
|
2871
|
-
const unsupported =
|
|
2871
|
+
const unsupported = useMemo5(() => unsupportedKinds(), []);
|
|
2872
2872
|
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;
|
|
2873
2873
|
const [activeTab, setActiveTab] = useState3(
|
|
2874
2874
|
kindsPresent[0] ?? "forced-reflow"
|
|
@@ -3249,6 +3249,65 @@ function Panel(props) {
|
|
|
3249
3249
|
|
|
3250
3250
|
// src/app.tsx
|
|
3251
3251
|
import { jsx as jsx10, jsxs as jsxs10 } from "preact/jsx-runtime";
|
|
3252
|
+
var PanelErrorBoundary = class extends Component {
|
|
3253
|
+
constructor() {
|
|
3254
|
+
super(...arguments);
|
|
3255
|
+
this.state = { error: null };
|
|
3256
|
+
}
|
|
3257
|
+
static getDerivedStateFromError(error) {
|
|
3258
|
+
return { error };
|
|
3259
|
+
}
|
|
3260
|
+
componentDidCatch(error) {
|
|
3261
|
+
console.warn("[react-perfscope] panel crashed:", error);
|
|
3262
|
+
}
|
|
3263
|
+
render() {
|
|
3264
|
+
if (this.state.error === null) return this.props.children;
|
|
3265
|
+
return /* @__PURE__ */ jsxs10(
|
|
3266
|
+
"div",
|
|
3267
|
+
{
|
|
3268
|
+
role: "alert",
|
|
3269
|
+
style: {
|
|
3270
|
+
position: "fixed",
|
|
3271
|
+
bottom: "16px",
|
|
3272
|
+
right: "16px",
|
|
3273
|
+
zIndex: 2147483647,
|
|
3274
|
+
display: "flex",
|
|
3275
|
+
alignItems: "center",
|
|
3276
|
+
gap: "8px",
|
|
3277
|
+
padding: "8px 12px",
|
|
3278
|
+
background: "#141414",
|
|
3279
|
+
border: "1px solid #ff3b30",
|
|
3280
|
+
borderRadius: "8px",
|
|
3281
|
+
color: "#e6e6e6",
|
|
3282
|
+
fontSize: "12px",
|
|
3283
|
+
fontFamily: "SF Mono, Menlo, Consolas, monospace"
|
|
3284
|
+
},
|
|
3285
|
+
children: [
|
|
3286
|
+
/* @__PURE__ */ jsx10("span", { children: "react-perfscope panel crashed \u2014 see console" }),
|
|
3287
|
+
/* @__PURE__ */ jsx10(
|
|
3288
|
+
"button",
|
|
3289
|
+
{
|
|
3290
|
+
"aria-label": "Close panel",
|
|
3291
|
+
onClick: () => {
|
|
3292
|
+
this.setState({ error: null });
|
|
3293
|
+
this.props.onClose();
|
|
3294
|
+
},
|
|
3295
|
+
style: {
|
|
3296
|
+
background: "none",
|
|
3297
|
+
border: "1px solid #444",
|
|
3298
|
+
borderRadius: "4px",
|
|
3299
|
+
color: "#e6e6e6",
|
|
3300
|
+
cursor: "pointer",
|
|
3301
|
+
padding: "2px 8px"
|
|
3302
|
+
},
|
|
3303
|
+
children: "\xD7"
|
|
3304
|
+
}
|
|
3305
|
+
)
|
|
3306
|
+
]
|
|
3307
|
+
}
|
|
3308
|
+
);
|
|
3309
|
+
}
|
|
3310
|
+
};
|
|
3252
3311
|
function App(props) {
|
|
3253
3312
|
const { recorder, position = "bottom-right", resolveFrame, finalize } = props;
|
|
3254
3313
|
const [recording, setRecording] = useState4(false);
|
|
@@ -3263,7 +3322,8 @@ function App(props) {
|
|
|
3263
3322
|
return;
|
|
3264
3323
|
}
|
|
3265
3324
|
const tick = () => {
|
|
3266
|
-
|
|
3325
|
+
const elapsed = performance.now() - startedAtRef.current;
|
|
3326
|
+
setElapsedMs(Math.floor(elapsed / 1e3) * 1e3);
|
|
3267
3327
|
rafRef.current = requestAnimationFrame(tick);
|
|
3268
3328
|
};
|
|
3269
3329
|
rafRef.current = requestAnimationFrame(tick);
|
|
@@ -3276,12 +3336,23 @@ function App(props) {
|
|
|
3276
3336
|
startedAtRef.current = performance.now();
|
|
3277
3337
|
setElapsedMs(0);
|
|
3278
3338
|
setResult(null);
|
|
3279
|
-
|
|
3339
|
+
try {
|
|
3340
|
+
recorder.start();
|
|
3341
|
+
} catch (err) {
|
|
3342
|
+
console.warn("[react-perfscope] failed to start recording:", err);
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3280
3345
|
setRecording(true);
|
|
3281
3346
|
} else {
|
|
3282
|
-
const r = recorder.stop();
|
|
3283
|
-
const token = ++resultTokenRef.current;
|
|
3284
3347
|
setRecording(false);
|
|
3348
|
+
let r;
|
|
3349
|
+
try {
|
|
3350
|
+
r = recorder.stop();
|
|
3351
|
+
} catch (err) {
|
|
3352
|
+
console.warn("[react-perfscope] failed to stop recording:", err);
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
const token = ++resultTokenRef.current;
|
|
3285
3356
|
setResult(r);
|
|
3286
3357
|
if (finalize) {
|
|
3287
3358
|
finalize(r).then((enriched) => {
|
|
@@ -3304,14 +3375,36 @@ function App(props) {
|
|
|
3304
3375
|
position
|
|
3305
3376
|
}
|
|
3306
3377
|
),
|
|
3307
|
-
result !== null && /* @__PURE__ */ jsx10(Panel, { result, position, onClose, resolveFrame })
|
|
3378
|
+
result !== null && /* @__PURE__ */ jsx10(PanelErrorBoundary, { onClose, children: /* @__PURE__ */ jsx10(Panel, { result, position, onClose, resolveFrame }) })
|
|
3308
3379
|
] });
|
|
3309
3380
|
}
|
|
3310
3381
|
|
|
3311
3382
|
// src/mount.tsx
|
|
3312
3383
|
import { jsx as jsx11 } from "preact/jsx-runtime";
|
|
3313
3384
|
function mount(opts) {
|
|
3314
|
-
|
|
3385
|
+
if (!opts.host && !document.body) {
|
|
3386
|
+
let torn = false;
|
|
3387
|
+
let unmount = null;
|
|
3388
|
+
const onReady = () => {
|
|
3389
|
+
if (torn || !document.body) return;
|
|
3390
|
+
unmount = mountNow(opts, document.body);
|
|
3391
|
+
};
|
|
3392
|
+
document.addEventListener("DOMContentLoaded", onReady, { once: true });
|
|
3393
|
+
return () => {
|
|
3394
|
+
torn = true;
|
|
3395
|
+
document.removeEventListener("DOMContentLoaded", onReady);
|
|
3396
|
+
unmount?.();
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
return mountNow(opts, opts.host ?? document.body);
|
|
3400
|
+
}
|
|
3401
|
+
function mountNow(opts, host) {
|
|
3402
|
+
const { recorder, position = "bottom-right", resolveFrame, finalize } = opts;
|
|
3403
|
+
if (host.querySelector("[data-perfscope-host]")) {
|
|
3404
|
+
console.warn("[react-perfscope] mount(): a widget is already mounted here \u2014 ignoring this call");
|
|
3405
|
+
return () => {
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3315
3408
|
return mountShadow(
|
|
3316
3409
|
/* @__PURE__ */ jsx11(App, { recorder, position, resolveFrame, finalize }),
|
|
3317
3410
|
{ parent: host }
|