mirror of
https://github.com/block/buzz.git
synced 2026-11-10 03:11:40 +00:00
Diagnostic profiling on a large community (101 issues, 258 PRs) showed Projects tab switches taking 2.5–3.6s, dominated by single React commits of 0.5–1.5s and per-render recomputation — fetch work was already off the main thread; the cost was building the UI. ### Measured: tab click → painted, per tab | tab | before | after | |---|---|---:| | projects | 3,608ms | 320–580ms | | repositories | 3,126–3,534ms | 310–410ms | | tasks | 395–1,101ms | ~115ms | | reviews | 322–2,603ms | ~96ms | | activity | 597–741ms | ~148ms | Single-commit ceiling dropped from 1,541ms to ≤200ms (growth steps 25–40ms). Fixes in profiled-cost order: - **Profile popover body mounts only while open.** `UserProfilePopover` carried seven query subscriptions plus interaction hooks per instance even when closed; grids mount hundreds (five per card in people stacks, one per row author) — measured **~40ms per card**, the dominant share of the 1.2s card-tab commits. The always-mounted shell is now just the Radix root + trigger; trigger markup, hover timing, and keyboard handling are unchanged, and hover/tooltip event continuity is preserved because the trigger never remounts. - **Incremental row mounting.** The first 12 cards / 30 rows render in the first commit; the rest stream in 36–60-per-frame low-priority transitions. Grouped lists trim across group boundaries via a pure, tested slicer; the mounted count survives in-place refetches. - **Activity feed**: was rebuilt unmemoized on every render, markdown-flattening every issue/PR/comment body in the community just to sort and keep 30 items (~360+ flattens per render on the measured community). Now memoized, and bodies stay raw until after the sort+slice — 30 flattens, once per data change. - **Contribution graph** (always-visible rail, so every tab paid for it): ~180 day cells each wrapped in a Radix tooltip with per-cell Intl date formatting per render. Now memoized, cells precomputed once per data change, native `title` tooltips. (The activity-bar segments keep their styled Radix tooltips — pinned by an existing spec.) - **Rows/cards memoized with identity-stable props**: per-row selection arrays were rebuilt per row per render (O(n²) — 258 PRs × 258-item arrays each render) and are now hoisted and shared; people arrays derive inside the memoized cards; the rail's stat walk over every issue/PR is memoized. - **`content-visibility: auto`** on cards and rows so offscreen entries skip layout and paint; **tab switches run in a React transition** so the click stays responsive while the new tree mounts. Remaining known cost (out of scope): cold-entry data readiness — the work-item and activity queries ship thousands of events to compute counts (2–4s on a large community; see the fan-lifecycle PR). The structural fix is a relay-side aggregate; tracked as follow-up. --------- Signed-off-by: Max Lampert <maxwell@squareup.com>
158 lines
5.5 KiB
JavaScript
158 lines
5.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { countGroupedRows, sliceGroupedRows } from "./useIncrementalMount.ts";
|
|
|
|
function group(id, rowCount) {
|
|
return { id, rows: Array.from({ length: rowCount }, (_, i) => `${id}-${i}`) };
|
|
}
|
|
|
|
test("sliceGroupedRows trims across group boundaries in order", () => {
|
|
const groups = [group("a", 3), group("b", 4), group("c", 2)];
|
|
|
|
assert.deepEqual(
|
|
sliceGroupedRows(groups, 5).map((g) => [g.id, g.rows.length]),
|
|
[
|
|
["a", 3],
|
|
["b", 2],
|
|
],
|
|
);
|
|
});
|
|
|
|
test("sliceGroupedRows keeps whole groups untouched when under budget", () => {
|
|
const groups = [group("a", 3), group("b", 4)];
|
|
const sliced = sliceGroupedRows(groups, 10);
|
|
assert.equal(sliced.length, 2);
|
|
// Fully-included groups keep their identity (no needless clones).
|
|
assert.equal(sliced[0], groups[0]);
|
|
assert.equal(sliced[1], groups[1]);
|
|
});
|
|
|
|
test("sliceGroupedRows with zero budget is empty; counts add up", () => {
|
|
const groups = [group("a", 3), group("b", 4)];
|
|
assert.deepEqual(sliceGroupedRows(groups, 0), []);
|
|
assert.equal(countGroupedRows(groups), 7);
|
|
});
|
|
|
|
// --- Hook behavior: growth, layout gating, shrink reset -------------------
|
|
|
|
async function withHookHarness(run) {
|
|
const { JSDOM } = await import("jsdom");
|
|
const dom = new JSDOM("<!doctype html><html><body></body></html>");
|
|
const originals = {
|
|
window: globalThis.window,
|
|
document: globalThis.document,
|
|
navigator: Object.getOwnPropertyDescriptor(globalThis, "navigator"),
|
|
act: globalThis.IS_REACT_ACT_ENVIRONMENT,
|
|
};
|
|
const frames = [];
|
|
dom.window.requestAnimationFrame = (cb) => frames.push(cb) && frames.length;
|
|
dom.window.cancelAnimationFrame = (id) => {
|
|
frames[id - 1] = null;
|
|
};
|
|
globalThis.window = dom.window;
|
|
globalThis.document = dom.window.document;
|
|
Object.defineProperty(globalThis, "navigator", {
|
|
configurable: true,
|
|
value: dom.window.navigator,
|
|
});
|
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
try {
|
|
const React = (await import("react")).default;
|
|
const { act } = await import("react");
|
|
const { createRoot } = await import("react-dom/client");
|
|
const { useIncrementalMount } = await import("./useIncrementalMount.ts");
|
|
|
|
const seen = [];
|
|
function Probe({ total, enabled }) {
|
|
seen.push(useIncrementalMount(total, 2, 3, enabled));
|
|
return null;
|
|
}
|
|
const root = createRoot(
|
|
dom.window.document.body.appendChild(
|
|
dom.window.document.createElement("div"),
|
|
),
|
|
);
|
|
const render = async (total, enabled = true) => {
|
|
await act(async () => {
|
|
root.render(React.createElement(Probe, { enabled, total }));
|
|
});
|
|
};
|
|
const flushFrames = async () => {
|
|
// Growth chains one rAF per step; run until quiescent.
|
|
for (let i = 0; i < 20 && frames.some(Boolean); i += 1) {
|
|
const pending = frames.splice(0, frames.length).filter(Boolean);
|
|
await act(async () => {
|
|
for (const cb of pending) cb();
|
|
});
|
|
}
|
|
};
|
|
await run({ render, flushFrames, seen, root, act });
|
|
await act(async () => root.unmount());
|
|
} finally {
|
|
globalThis.window = originals.window;
|
|
globalThis.document = originals.document;
|
|
if (originals.navigator) {
|
|
Object.defineProperty(globalThis, "navigator", originals.navigator);
|
|
}
|
|
globalThis.IS_REACT_ACT_ENVIRONMENT = originals.act;
|
|
}
|
|
}
|
|
|
|
test("hook grows stepwise to total and persists across data updates", async () => {
|
|
await withHookHarness(async ({ render, flushFrames, seen }) => {
|
|
await render(8);
|
|
assert.equal(seen.at(-1), 2); // initial window paints first
|
|
await flushFrames();
|
|
assert.equal(seen.at(-1), 8); // grown 2→5→8
|
|
await render(8); // in-place update: stays fully mounted
|
|
assert.equal(seen.at(-1), 8);
|
|
});
|
|
});
|
|
|
|
test("hook holds at initial while disabled and grows on activation", async () => {
|
|
await withHookHarness(async ({ render, flushFrames, seen }) => {
|
|
await render(9, false);
|
|
await flushFrames();
|
|
assert.equal(seen.at(-1), 2, "disabled layout must not grow");
|
|
await render(9, true);
|
|
await flushFrames();
|
|
assert.equal(seen.at(-1), 9);
|
|
// Deactivating resets to the initial window for the next activation.
|
|
await render(9, false);
|
|
await flushFrames();
|
|
assert.equal(seen.at(-1), 2);
|
|
});
|
|
});
|
|
|
|
test("hook snaps down on total shrink and re-grows stepwise on restore", async () => {
|
|
await withHookHarness(async ({ render, flushFrames, seen }) => {
|
|
await render(9);
|
|
await flushFrames();
|
|
assert.equal(seen.at(-1), 9);
|
|
await render(4); // scope narrowed
|
|
await flushFrames();
|
|
assert.equal(seen.at(-1), 4);
|
|
await render(9); // scope restored: no one-shot bulk mount
|
|
assert.equal(seen.at(-1), 4);
|
|
await flushFrames();
|
|
assert.equal(seen.at(-1), 9);
|
|
});
|
|
});
|
|
|
|
test("list dwell then grid switch: first enabled commit stays at the initial window", async () => {
|
|
await withHookHarness(async ({ render, flushFrames, seen }) => {
|
|
// Dwell in list layout (counter disabled) long enough that an ungated
|
|
// counter would have grown to the full collection.
|
|
await render(20, false);
|
|
await flushFrames();
|
|
assert.equal(seen.at(-1), 2, "dormant counter must hold at initial");
|
|
// Switch to grid: the FIRST grid commit must mount only the initial
|
|
// window — never the whole collection in one commit.
|
|
await render(20, true);
|
|
assert.equal(seen.at(-1), 2, "first grid commit stays at initial");
|
|
await flushFrames();
|
|
assert.equal(seen.at(-1), 20, "then grows stepwise to the total");
|
|
});
|
|
});
|