@react-perfscope/ui 0.1.1 → 0.3.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 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** — component, reason, duration, timestamp
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** — 컴포넌트, reason, duration, 타임스탬프
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
@@ -132,6 +132,8 @@ var en = {
132
132
  blocking: "blocking",
133
133
  sourceCount: (n) => `${n} source(s)`,
134
134
  moreItems: (n) => `+ ${n} more`,
135
+ coalescedReads: (n) => `${n} reads`,
136
+ cascadeComponents: (n) => `${n} components`,
135
137
  startRecording: "Start recording",
136
138
  stopRecording: "Stop recording",
137
139
  rec: "rec",
@@ -220,6 +222,8 @@ var ko = {
220
222
  blocking: "\uCC28\uB2E8",
221
223
  sourceCount: (n) => `\uC18C\uC2A4 ${n}\uAC1C`,
222
224
  moreItems: (n) => `\uC678 ${n}\uAC1C \uB354`,
225
+ coalescedReads: (n) => `\uC77D\uAE30 ${n}\uD68C`,
226
+ cascadeComponents: (n) => `\uCEF4\uD3EC\uB10C\uD2B8 ${n}\uAC1C`,
223
227
  startRecording: "\uB179\uD654 \uC2DC\uC791",
224
228
  stopRecording: "\uB179\uD654 \uC885\uB8CC",
225
229
  rec: "\uB179\uD654",
@@ -828,8 +832,10 @@ function renderBreakdown(members) {
828
832
  const counts = /* @__PURE__ */ new Map();
829
833
  for (const s of members) {
830
834
  if (s.kind !== "render") continue;
831
- if (!counts.has(s.component)) order.push(s.component);
832
- counts.set(s.component, (counts.get(s.component) ?? 0) + 1);
835
+ for (const m of s.members ?? [s]) {
836
+ if (!counts.has(m.component)) order.push(m.component);
837
+ counts.set(m.component, (counts.get(m.component) ?? 0) + 1);
838
+ }
833
839
  }
834
840
  return order.map((c) => ({ component: c, count: counts.get(c) }));
835
841
  }
@@ -1731,6 +1737,7 @@ function TooltipContent({ s, startedAt }) {
1731
1737
  s.duration.toFixed(2),
1732
1738
  "ms"
1733
1739
  ] }),
1740
+ (s.count ?? 1) > 1 ? ` \xD7${s.count}` : "",
1734
1741
  " ",
1735
1742
  at
1736
1743
  ] });
@@ -2008,7 +2015,7 @@ function summary(s) {
2008
2015
  case "web-vital":
2009
2016
  return `${s.name}: ${s.value.toFixed(2)}`;
2010
2017
  case "render":
2011
- return `${s.component} \u2022 ${s.reason} \u2022 ${s.duration.toFixed(2)}ms`;
2018
+ return `${s.component} \u2022 ${s.reason} \u2022 ${s.duration.toFixed(2)}ms${s.count && s.count > 1 ? ` \u2022 ${s.count} components` : ""}`;
2012
2019
  }
2013
2020
  }
2014
2021
  var detailLabelStyle = { color: "#888", marginRight: "6px" };
@@ -2093,6 +2100,25 @@ function WebVitalDetail({ s }) {
2093
2100
  }
2094
2101
  function RenderDetail({ s }) {
2095
2102
  const { t } = useI18n();
2103
+ if (s.members && s.members.length > 0) {
2104
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { paddingLeft: "12px" }, children: [
2105
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2106
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.duration }),
2107
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2108
+ s.duration.toFixed(3),
2109
+ "ms"
2110
+ ] })
2111
+ ] }),
2112
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2113
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.at }),
2114
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2115
+ s.at.toFixed(2),
2116
+ "ms"
2117
+ ] })
2118
+ ] }),
2119
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { marginTop: "6px" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CascadeMembers, { members: s.members }) })
2120
+ ] });
2121
+ }
2096
2122
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { paddingLeft: "12px" }, children: [
2097
2123
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2098
2124
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.component }),
@@ -2383,6 +2409,7 @@ function SummaryLine({ signal }) {
2383
2409
  ] });
2384
2410
  }
2385
2411
  if (signal.kind === "forced-reflow") {
2412
+ const count = signal.count ?? 1;
2386
2413
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2387
2414
  "@ ",
2388
2415
  signal.at.toFixed(1),
@@ -2391,6 +2418,10 @@ function SummaryLine({ signal }) {
2391
2418
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color }, children: [
2392
2419
  signal.duration.toFixed(2),
2393
2420
  "ms"
2421
+ ] }),
2422
+ count > 1 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: "#888" }, children: [
2423
+ " \u2022 ",
2424
+ t.coalescedReads(count)
2394
2425
  ] })
2395
2426
  ] });
2396
2427
  }
@@ -2412,7 +2443,8 @@ function SummaryLine({ signal }) {
2412
2443
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color }, children: [
2413
2444
  signal.duration.toFixed(2),
2414
2445
  "ms"
2415
- ] })
2446
+ ] }),
2447
+ signal.count && signal.count > 1 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "#888" }, children: t.cascadeComponents(signal.count) })
2416
2448
  ] });
2417
2449
  }
2418
2450
  if (signal.kind === "network") {
@@ -2432,9 +2464,6 @@ function SummaryLine({ signal }) {
2432
2464
  }
2433
2465
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: summary(signal) });
2434
2466
  }
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
2467
  function tabSupportsGrouping(kind) {
2439
2468
  return kind === "render" || kind === "forced-reflow";
2440
2469
  }
@@ -2478,22 +2507,17 @@ function groupSignals(signals, mode, kind) {
2478
2507
  const byName = /* @__PURE__ */ new Map();
2479
2508
  for (const s of signals) {
2480
2509
  if (s.kind !== "render") continue;
2481
- const k = s.component;
2482
- if (!byName.has(k)) byName.set(k, []);
2483
- byName.get(k).push(s);
2510
+ for (const m of s.members ?? [s]) {
2511
+ if (!byName.has(m.component)) byName.set(m.component, []);
2512
+ byName.get(m.component).push(m);
2513
+ }
2484
2514
  }
2485
2515
  return Array.from(byName.entries()).sort((a, b) => b[1].length - a[1].length).map(([label, list]) => ({ label, count: list.length, signals: list }));
2486
2516
  }
2487
2517
  if (mode === "commit" && kind === "render") {
2488
- const byCommit = /* @__PURE__ */ new Map();
2489
- for (const s of signals) {
2490
- if (s.kind !== "render") continue;
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 };
2518
+ return signals.filter((s) => s.kind === "render").sort((a, b) => a.commitId - b.commitId).map((s) => {
2519
+ const list = s.members ?? [s];
2520
+ return { label: s.component, count: list.length, signals: list };
2497
2521
  });
2498
2522
  }
2499
2523
  if (mode === "source" && kind === "forced-reflow") {
@@ -2855,7 +2879,9 @@ function Panel(props) {
2855
2879
  activeKind === "render" && activeTab !== "timeline" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2856
2880
  RenderInsights,
2857
2881
  {
2858
- signals: grouped.render.filter((s) => s.kind === "render"),
2882
+ signals: grouped.render.flatMap(
2883
+ (s) => s.kind === "render" ? s.members ?? [s] : []
2884
+ ),
2859
2885
  onSelect: () => {
2860
2886
  setGroupMode({ ...groupMode, render: "component" });
2861
2887
  setExpandedKey(null);