@react-perfscope/ui 0.2.0 → 0.4.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/README.md +4 -6
- package/dist/index.cjs +111 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +111 -23
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -27,13 +27,12 @@ const unmount = mount({ recorder })
|
|
|
27
27
|
|
|
28
28
|
Click any row to expand its detail. Each signal kind shows different info:
|
|
29
29
|
|
|
30
|
-
- **forced-reflow** — call stack showing where layout was forced
|
|
30
|
+
- **forced-reflow** — call stack showing where layout was forced; reads in one turn are coalesced into a single entry carrying its read count
|
|
31
31
|
- **layout-shift** — CLS value + each source rect
|
|
32
32
|
- **long-task** — start/end/duration, plus call stack if captured
|
|
33
33
|
- **network** — full URL, transfer size, render-blocking flag
|
|
34
|
-
- **paint** — paint name (first-paint / first-contentful-paint)
|
|
35
34
|
- **web-vital** — metric name, value, rating per [Google thresholds](https://web.dev/vitals/)
|
|
36
|
-
- **render** —
|
|
35
|
+
- **render** — one entry per commit, showing the cascade root and every component that re-rendered
|
|
37
36
|
|
|
38
37
|
The header has a **Save** button that downloads the full recording as JSON. The `render` tab offers **Group by component** and the `forced-reflow` tab offers **Group by source**.
|
|
39
38
|
|
|
@@ -79,13 +78,12 @@ const unmount = mount({ recorder })
|
|
|
79
78
|
|
|
80
79
|
신호 row를 클릭하면 디테일이 펼쳐진다. 종류별로 다른 정보를 보여준다:
|
|
81
80
|
|
|
82
|
-
- **forced-reflow** — 레이아웃을 강제한 호출
|
|
81
|
+
- **forced-reflow** — 레이아웃을 강제한 호출 스택. 한 턴에 일어난 읽기는 읽기 횟수를 담은 한 항목으로 합쳐진다
|
|
83
82
|
- **layout-shift** — CLS 값 + 각 source rect
|
|
84
83
|
- **long-task** — 시작/끝/duration, 캡처된 스택이 있으면 같이 표시
|
|
85
84
|
- **network** — 전체 URL, transfer size, render-blocking 여부
|
|
86
|
-
- **paint** — paint 이름 (first-paint / first-contentful-paint)
|
|
87
85
|
- **web-vital** — 메트릭 이름, 값, [Google 기준](https://web.dev/vitals/)에 따른 등급
|
|
88
|
-
- **render** —
|
|
86
|
+
- **render** — 커밋당 한 항목. 캐스케이드 루트와 그 커밋에서 리렌더된 모든 컴포넌트를 보여준다
|
|
89
87
|
|
|
90
88
|
헤더의 **Save** 버튼으로 전체 recording을 JSON으로 받을 수 있다. `render` 탭에는 **Group by component**, `forced-reflow` 탭에는 **Group by source** 옵션이 있다.
|
|
91
89
|
|
package/dist/index.cjs
CHANGED
|
@@ -105,6 +105,9 @@ var en = {
|
|
|
105
105
|
sort: "Sort",
|
|
106
106
|
sortChronological: "chronological",
|
|
107
107
|
sortSeverity: "severity (worst first)",
|
|
108
|
+
filterPlaceholder: "Filter\u2026",
|
|
109
|
+
filterAria: "Filter signals",
|
|
110
|
+
filterNoMatches: "No signals match the filter.",
|
|
108
111
|
groupBy: "Group by",
|
|
109
112
|
groupChronological: "chronological",
|
|
110
113
|
groupComponent: "component",
|
|
@@ -132,6 +135,8 @@ var en = {
|
|
|
132
135
|
blocking: "blocking",
|
|
133
136
|
sourceCount: (n) => `${n} source(s)`,
|
|
134
137
|
moreItems: (n) => `+ ${n} more`,
|
|
138
|
+
coalescedReads: (n) => `${n} reads`,
|
|
139
|
+
cascadeComponents: (n) => `${n} components`,
|
|
135
140
|
startRecording: "Start recording",
|
|
136
141
|
stopRecording: "Stop recording",
|
|
137
142
|
rec: "rec",
|
|
@@ -193,6 +198,9 @@ var ko = {
|
|
|
193
198
|
sort: "\uC815\uB82C",
|
|
194
199
|
sortChronological: "\uC2DC\uAC04\uC21C",
|
|
195
200
|
sortSeverity: "\uC2EC\uAC01\uB3C4\uC21C (\uB192\uC740 \uAC83\uBD80\uD130)",
|
|
201
|
+
filterPlaceholder: "\uD544\uD130\u2026",
|
|
202
|
+
filterAria: "\uC2DC\uADF8\uB110 \uD544\uD130",
|
|
203
|
+
filterNoMatches: "\uD544\uD130\uC640 \uC77C\uCE58\uD558\uB294 \uC2DC\uADF8\uB110\uC774 \uC5C6\uC5B4\uC694.",
|
|
196
204
|
groupBy: "\uADF8\uB8F9\uD654",
|
|
197
205
|
groupChronological: "\uC2DC\uAC04\uC21C",
|
|
198
206
|
groupComponent: "\uCEF4\uD3EC\uB10C\uD2B8",
|
|
@@ -220,6 +228,8 @@ var ko = {
|
|
|
220
228
|
blocking: "\uCC28\uB2E8",
|
|
221
229
|
sourceCount: (n) => `\uC18C\uC2A4 ${n}\uAC1C`,
|
|
222
230
|
moreItems: (n) => `\uC678 ${n}\uAC1C \uB354`,
|
|
231
|
+
coalescedReads: (n) => `\uC77D\uAE30 ${n}\uD68C`,
|
|
232
|
+
cascadeComponents: (n) => `\uCEF4\uD3EC\uB10C\uD2B8 ${n}\uAC1C`,
|
|
223
233
|
startRecording: "\uB179\uD654 \uC2DC\uC791",
|
|
224
234
|
stopRecording: "\uB179\uD654 \uC885\uB8CC",
|
|
225
235
|
rec: "\uB179\uD654",
|
|
@@ -828,8 +838,10 @@ function renderBreakdown(members) {
|
|
|
828
838
|
const counts = /* @__PURE__ */ new Map();
|
|
829
839
|
for (const s of members) {
|
|
830
840
|
if (s.kind !== "render") continue;
|
|
831
|
-
|
|
832
|
-
|
|
841
|
+
for (const m of s.members ?? [s]) {
|
|
842
|
+
if (!counts.has(m.component)) order.push(m.component);
|
|
843
|
+
counts.set(m.component, (counts.get(m.component) ?? 0) + 1);
|
|
844
|
+
}
|
|
833
845
|
}
|
|
834
846
|
return order.map((c) => ({ component: c, count: counts.get(c) }));
|
|
835
847
|
}
|
|
@@ -1731,6 +1743,7 @@ function TooltipContent({ s, startedAt }) {
|
|
|
1731
1743
|
s.duration.toFixed(2),
|
|
1732
1744
|
"ms"
|
|
1733
1745
|
] }),
|
|
1746
|
+
(s.count ?? 1) > 1 ? ` \xD7${s.count}` : "",
|
|
1734
1747
|
" ",
|
|
1735
1748
|
at
|
|
1736
1749
|
] });
|
|
@@ -1880,6 +1893,45 @@ function RenderInsights({ signals, onSelect }) {
|
|
|
1880
1893
|
);
|
|
1881
1894
|
}
|
|
1882
1895
|
|
|
1896
|
+
// src/filter.ts
|
|
1897
|
+
function signalSearchText(signal) {
|
|
1898
|
+
switch (signal.kind) {
|
|
1899
|
+
case "render": {
|
|
1900
|
+
const parts = [signal.component];
|
|
1901
|
+
if (signal.changedProps) parts.push(...signal.changedProps);
|
|
1902
|
+
if (signal.members) for (const m of signal.members) parts.push(m.component);
|
|
1903
|
+
return parts.join(" ").toLowerCase();
|
|
1904
|
+
}
|
|
1905
|
+
case "network":
|
|
1906
|
+
return signal.url.toLowerCase();
|
|
1907
|
+
case "web-vital":
|
|
1908
|
+
return signal.name.toLowerCase();
|
|
1909
|
+
case "interaction":
|
|
1910
|
+
return [signal.eventType, signal.target ?? ""].join(" ").toLowerCase();
|
|
1911
|
+
case "forced-reflow": {
|
|
1912
|
+
const f = signal.stack[0];
|
|
1913
|
+
return f ? [f.fnName ?? "", f.file].join(" ").toLowerCase() : "";
|
|
1914
|
+
}
|
|
1915
|
+
case "long-task": {
|
|
1916
|
+
const parts = [];
|
|
1917
|
+
if (signal.scripts)
|
|
1918
|
+
for (const s of signal.scripts) parts.push(s.sourceFunctionName, s.invoker, s.sourceURL);
|
|
1919
|
+
if (signal.attribution)
|
|
1920
|
+
for (const a of signal.attribution) parts.push(a.frame.fnName ?? "", a.frame.file);
|
|
1921
|
+
return parts.join(" ").toLowerCase();
|
|
1922
|
+
}
|
|
1923
|
+
case "layout-shift":
|
|
1924
|
+
return "layout-shift";
|
|
1925
|
+
default:
|
|
1926
|
+
return "";
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
function signalMatchesFilter(signal, query) {
|
|
1930
|
+
const q = query.trim().toLowerCase();
|
|
1931
|
+
if (!q) return true;
|
|
1932
|
+
return signalSearchText(signal).includes(q);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1883
1935
|
// src/panel.tsx
|
|
1884
1936
|
var import_jsx_runtime5 = require("preact/jsx-runtime");
|
|
1885
1937
|
var RENDER_REASON_COLOR = {
|
|
@@ -2008,7 +2060,7 @@ function summary(s) {
|
|
|
2008
2060
|
case "web-vital":
|
|
2009
2061
|
return `${s.name}: ${s.value.toFixed(2)}`;
|
|
2010
2062
|
case "render":
|
|
2011
|
-
return `${s.component} \u2022 ${s.reason} \u2022 ${s.duration.toFixed(2)}ms`;
|
|
2063
|
+
return `${s.component} \u2022 ${s.reason} \u2022 ${s.duration.toFixed(2)}ms${s.count && s.count > 1 ? ` \u2022 ${s.count} components` : ""}`;
|
|
2012
2064
|
}
|
|
2013
2065
|
}
|
|
2014
2066
|
var detailLabelStyle = { color: "#888", marginRight: "6px" };
|
|
@@ -2093,6 +2145,25 @@ function WebVitalDetail({ s }) {
|
|
|
2093
2145
|
}
|
|
2094
2146
|
function RenderDetail({ s }) {
|
|
2095
2147
|
const { t } = useI18n();
|
|
2148
|
+
if (s.members && s.members.length > 0) {
|
|
2149
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { paddingLeft: "12px" }, children: [
|
|
2150
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
|
|
2151
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.duration }),
|
|
2152
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
|
|
2153
|
+
s.duration.toFixed(3),
|
|
2154
|
+
"ms"
|
|
2155
|
+
] })
|
|
2156
|
+
] }),
|
|
2157
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
|
|
2158
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.at }),
|
|
2159
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
|
|
2160
|
+
s.at.toFixed(2),
|
|
2161
|
+
"ms"
|
|
2162
|
+
] })
|
|
2163
|
+
] }),
|
|
2164
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { marginTop: "6px" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CascadeMembers, { members: s.members }) })
|
|
2165
|
+
] });
|
|
2166
|
+
}
|
|
2096
2167
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { paddingLeft: "12px" }, children: [
|
|
2097
2168
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
|
|
2098
2169
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.component }),
|
|
@@ -2383,6 +2454,7 @@ function SummaryLine({ signal }) {
|
|
|
2383
2454
|
] });
|
|
2384
2455
|
}
|
|
2385
2456
|
if (signal.kind === "forced-reflow") {
|
|
2457
|
+
const count = signal.count ?? 1;
|
|
2386
2458
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
|
|
2387
2459
|
"@ ",
|
|
2388
2460
|
signal.at.toFixed(1),
|
|
@@ -2391,6 +2463,10 @@ function SummaryLine({ signal }) {
|
|
|
2391
2463
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color }, children: [
|
|
2392
2464
|
signal.duration.toFixed(2),
|
|
2393
2465
|
"ms"
|
|
2466
|
+
] }),
|
|
2467
|
+
count > 1 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: "#888" }, children: [
|
|
2468
|
+
" \u2022 ",
|
|
2469
|
+
t.coalescedReads(count)
|
|
2394
2470
|
] })
|
|
2395
2471
|
] });
|
|
2396
2472
|
}
|
|
@@ -2412,7 +2488,8 @@ function SummaryLine({ signal }) {
|
|
|
2412
2488
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color }, children: [
|
|
2413
2489
|
signal.duration.toFixed(2),
|
|
2414
2490
|
"ms"
|
|
2415
|
-
] })
|
|
2491
|
+
] }),
|
|
2492
|
+
signal.count && signal.count > 1 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "#888" }, children: t.cascadeComponents(signal.count) })
|
|
2416
2493
|
] });
|
|
2417
2494
|
}
|
|
2418
2495
|
if (signal.kind === "network") {
|
|
@@ -2432,9 +2509,6 @@ function SummaryLine({ signal }) {
|
|
|
2432
2509
|
}
|
|
2433
2510
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: summary(signal) });
|
|
2434
2511
|
}
|
|
2435
|
-
function cascadeRootOf(list) {
|
|
2436
|
-
return list.find((s) => s.reason === "state") ?? list.find((s) => s.reason === "mount") ?? [...list].sort((a, b) => a.depth - b.depth)[0];
|
|
2437
|
-
}
|
|
2438
2512
|
function tabSupportsGrouping(kind) {
|
|
2439
2513
|
return kind === "render" || kind === "forced-reflow";
|
|
2440
2514
|
}
|
|
@@ -2478,22 +2552,17 @@ function groupSignals(signals, mode, kind) {
|
|
|
2478
2552
|
const byName = /* @__PURE__ */ new Map();
|
|
2479
2553
|
for (const s of signals) {
|
|
2480
2554
|
if (s.kind !== "render") continue;
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
|
|
2555
|
+
for (const m of s.members ?? [s]) {
|
|
2556
|
+
if (!byName.has(m.component)) byName.set(m.component, []);
|
|
2557
|
+
byName.get(m.component).push(m);
|
|
2558
|
+
}
|
|
2484
2559
|
}
|
|
2485
2560
|
return Array.from(byName.entries()).sort((a, b) => b[1].length - a[1].length).map(([label, list]) => ({ label, count: list.length, signals: list }));
|
|
2486
2561
|
}
|
|
2487
2562
|
if (mode === "commit" && kind === "render") {
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
if (!byCommit.has(s.commitId)) byCommit.set(s.commitId, []);
|
|
2492
|
-
byCommit.get(s.commitId).push(s);
|
|
2493
|
-
}
|
|
2494
|
-
return Array.from(byCommit.entries()).sort((a, b) => a[0] - b[0]).map(([id, list]) => {
|
|
2495
|
-
const root = cascadeRootOf(list);
|
|
2496
|
-
return { label: root?.component ?? `commit ${id}`, count: list.length, signals: list };
|
|
2563
|
+
return signals.filter((s) => s.kind === "render").sort((a, b) => a.commitId - b.commitId).map((s) => {
|
|
2564
|
+
const list = s.members ?? [s];
|
|
2565
|
+
return { label: s.component, count: list.length, signals: list };
|
|
2497
2566
|
});
|
|
2498
2567
|
}
|
|
2499
2568
|
if (mode === "source" && kind === "forced-reflow") {
|
|
@@ -2628,6 +2697,7 @@ function Panel(props) {
|
|
|
2628
2697
|
const [expandedKey, setExpandedKey] = (0, import_hooks3.useState)(null);
|
|
2629
2698
|
const [groupMode, setGroupMode] = (0, import_hooks3.useState)({});
|
|
2630
2699
|
const [sortMode, setSortMode] = (0, import_hooks3.useState)({});
|
|
2700
|
+
const [filterText, setFilterText] = (0, import_hooks3.useState)({});
|
|
2631
2701
|
const activeOverlayCount = (0, import_hooks3.useRef)(0);
|
|
2632
2702
|
(0, import_hooks3.useEffect)(() => () => hideAllOverlays(), []);
|
|
2633
2703
|
function handleHover(signal) {
|
|
@@ -2834,7 +2904,21 @@ function Panel(props) {
|
|
|
2834
2904
|
]
|
|
2835
2905
|
}
|
|
2836
2906
|
)
|
|
2837
|
-
] })
|
|
2907
|
+
] }),
|
|
2908
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2909
|
+
"input",
|
|
2910
|
+
{
|
|
2911
|
+
type: "text",
|
|
2912
|
+
"aria-label": t.filterAria,
|
|
2913
|
+
placeholder: t.filterPlaceholder,
|
|
2914
|
+
value: filterText[activeKind] ?? "",
|
|
2915
|
+
onInput: (e) => {
|
|
2916
|
+
setFilterText({ ...filterText, [activeKind]: e.target.value });
|
|
2917
|
+
setExpandedKey(null);
|
|
2918
|
+
},
|
|
2919
|
+
style: { marginLeft: "auto", background: "#1a1a1a", color: "#e6e6e6", border: "1px solid #2a2a2a", borderRadius: "4px", padding: "2px 6px", fontSize: "11px", width: "120px" }
|
|
2920
|
+
}
|
|
2921
|
+
)
|
|
2838
2922
|
] }),
|
|
2839
2923
|
activeTab === "timeline" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { flexGrow: 1, overflowY: "auto", paddingTop: "4px", paddingBottom: "24px" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2840
2924
|
Timeline,
|
|
@@ -2855,7 +2939,7 @@ function Panel(props) {
|
|
|
2855
2939
|
activeKind === "render" && activeTab !== "timeline" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2856
2940
|
RenderInsights,
|
|
2857
2941
|
{
|
|
2858
|
-
signals: grouped.render.
|
|
2942
|
+
signals: grouped.render.flatMap((s) => s.kind === "render" ? s.members ?? [s] : []).filter((s) => signalMatchesFilter(s, filterText.render ?? "")),
|
|
2859
2943
|
onSelect: () => {
|
|
2860
2944
|
setGroupMode({ ...groupMode, render: "component" });
|
|
2861
2945
|
setExpandedKey(null);
|
|
@@ -2878,7 +2962,10 @@ function Panel(props) {
|
|
|
2878
2962
|
}
|
|
2879
2963
|
),
|
|
2880
2964
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("ul", { style: { listStyle: "none", margin: 0, padding: 0, overflowY: "auto", flexGrow: 1, display: activeTab === "timeline" ? "none" : void 0 }, children: activeKind && (() => {
|
|
2881
|
-
const
|
|
2965
|
+
const filtered = grouped[activeKind].filter(
|
|
2966
|
+
(s) => signalMatchesFilter(s, filterText[activeKind] ?? "")
|
|
2967
|
+
);
|
|
2968
|
+
const baseSignals = (sortMode[activeKind] ?? "chronological") === "severity" ? sortSignalsBySeverity(filtered) : filtered;
|
|
2882
2969
|
return groupSignals(baseSignals, groupMode[activeKind] ?? "chronological", activeKind);
|
|
2883
2970
|
})().map((g, gi) => {
|
|
2884
2971
|
const currentMode = groupMode[activeKind] ?? "chronological";
|
|
@@ -2936,7 +3023,8 @@ function Panel(props) {
|
|
|
2936
3023
|
},
|
|
2937
3024
|
key
|
|
2938
3025
|
);
|
|
2939
|
-
}) })
|
|
3026
|
+
}) }),
|
|
3027
|
+
activeKind && activeTab !== "timeline" && (filterText[activeKind] ?? "").trim() !== "" && grouped[activeKind].length > 0 && grouped[activeKind].every((s) => !signalMatchesFilter(s, filterText[activeKind] ?? "")) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { padding: "12px 8px", fontSize: "11px", color: "#888" }, children: t.filterNoMatches })
|
|
2940
3028
|
] })
|
|
2941
3029
|
] });
|
|
2942
3030
|
}
|