/** * Per-widget inventory: one row per widget, with the signals that predict a performance problem. * * The earlier audit (audit-widget-render.mjs) looks for specific anti-patterns. This one answers a * different question — "which widgets deserve a closer look, and why" — so that a sweep of all of * them can be worked through in a defensible order instead of alphabetically. * * Every column is a *signal*, not a defect. A widget with 6 maps and no memo is fine if it renders * three rows; the point is to rank where to spend attention. * * node scripts/benchmarks/widget-inventory.mjs # ranked table * node scripts/benchmarks/widget-inventory.mjs --markdown # checklist for docs/perf/WIDGET-AUDIT.md */ import { readdirSync, readFileSync, statSync } from "node:fs"; import path from "node:path"; const root = "packages/widgets/src"; const asMarkdown = process.argv.includes("--markdown"); const walk = (dir) => readdirSync(dir).flatMap((entry) => { const full = path.join(dir, entry); return statSync(full).isDirectory() ? walk(full) : [full]; }); const count = (source, pattern) => (source.match(pattern) ?? []).length; /** Refetch intervals live in a registry keyed by tRPC path, not by widget folder. */ const refetchIntervals = () => { const source = readFileSync(path.join(root, "refetch-intervals.ts"), "utf8"); const entries = new Map(); const pattern = /\[\["(?:widget|docker)",\s*"(\w+)"\]\],\s*intervalSeconds:\s*(\d+|null)/g; let match; while ((match = pattern.exec(source)) !== null) { entries.set(match[1].toLowerCase(), match[2] === "null" ? null : Number(match[2])); } return entries; }; const intervals = refetchIntervals(); /** `mediaRequests` in the registry is the `media-requests` folder. */ const lookupInterval = (widget) => { const flattened = widget.replaceAll("-", ""); for (const [key, value] of intervals) { if (key === flattened || key.startsWith(flattened) || flattened.startsWith(key)) return value; } return undefined; }; const widgets = readdirSync(root) .filter((entry) => statSync(path.join(root, entry)).isDirectory()) .filter((entry) => !["_inputs", "common", "errors", "modals", "test"].includes(entry)); const rows = widgets.map((widget) => { const files = walk(path.join(root, widget)).filter((file) => /\.tsx?$/.test(file) && !file.includes(".spec.")); const sources = files.map((file) => readFileSync(file, "utf8")); const all = sources.join("\n"); const componentSource = sources.filter((_, index) => /component\.tsx$/.test(files[index])).join("\n"); const overlays = count(all, /<(Tooltip|HoverCard|Popover|Menu)[\s>]/g); // An overlay or image inside a `.map()` repeats per row, which is what turns a small cost into a // large one. // The parameter list may itself contain parens - `.map(({ id }) =>` - so the arrow is matched // loosely rather than by excluding `)`. const perRow = count(all, /\.map\([\s\S]{0,80}?=>[\s\S]{0,400}?<(Tooltip|HoverCard|Popover|Menu)[\s>]/g); return { widget, files: files.length, loc: all.split("\n").length, maps: count(all, /\.map\(/g), overlays, perRow, images: count(all, /]|]|MaskedOrNormalImage|backgroundImage/g), lazy: /loading="lazy"|offscreenRowStyle|contentVisibility/.test(all), scrollArea: / 0, }; }); /** * Heuristic priority. Weighted so that repetition beats size: a per-row overlay in a scrolling list * outranks a big file that renders once. */ const score = (row) => row.perRow * 12 + (row.scrollArea && !row.lazy ? 8 : 0) + (row.images > 0 && !row.lazy ? 6 : 0) + row.overlays * 2 + row.maps + (row.subscription ? 5 : 0) + (row.interval !== undefined && row.interval !== null && row.interval <= 5 ? 6 : 0) + (row.memo === 0 && row.useMemo === 0 && row.maps > 2 ? 4 : 0) + Math.floor(row.loc / 200); rows.sort((a, b) => score(b) - score(a) || a.widget.localeCompare(b.widget)); const describe = (row) => { const notes = []; if (row.perRow > 0) notes.push(`${row.perRow} overlay/row`); if (row.scrollArea && !row.lazy) notes.push("scrolls, not lazy"); if (row.images > 0 && !row.lazy) notes.push(`${row.images} image sites, not lazy`); if (row.subscription) notes.push("live subscription"); if (row.interval !== undefined && row.interval !== null && row.interval <= 5) notes.push(`refetch ${row.interval}s`); // Counting only `memo(` reported the downloads widget as unmemoised when it in fact uses useMemo // for its data, columns and stats. Either form counts. if (row.memo === 0 && row.useMemo === 0 && row.maps > 2) notes.push(`${row.maps} maps, no memoisation`); if (row.lazy) notes.push("lazy applied"); return notes.join(", ") || "-"; }; if (asMarkdown) { console.log("| # | widget | LOC | maps | overlays | images | notes |"); console.log("| --- | --- | --- | --- | --- | --- | --- |"); rows.forEach((row, index) => { console.log( `| ${index + 1} | \`${row.widget}\` | ${row.loc} | ${row.maps} | ${row.overlays} | ${row.images} | ${describe(row)} |`, ); }); } else { console.log(`${rows.length} widgets, ranked by predicted render cost\n`); console.log( `${"widget".padEnd(24)}${"score".padStart(6)}${"LOC".padStart(6)}${"maps".padStart(6)}${"ovl".padStart(5)}${"img".padStart(5)} notes`, ); for (const row of rows) { console.log( `${row.widget.padEnd(24)}${String(score(row)).padStart(6)}${String(row.loc).padStart(6)}` + `${String(row.maps).padStart(6)}${String(row.overlays).padStart(5)}${String(row.images).padStart(5)} ${describe(row)}`, ); } const totals = rows.reduce( (accumulator, row) => ({ loc: accumulator.loc + row.loc, maps: accumulator.maps + row.maps, overlays: accumulator.overlays + row.overlays, perRow: accumulator.perRow + row.perRow, }), { loc: 0, maps: 0, overlays: 0, perRow: 0 }, ); console.log( `\n${totals.loc} lines, ${totals.maps} maps, ${totals.overlays} overlays, ${totals.perRow} of them per-row`, ); console.log(`${rows.filter((row) => row.scrollArea && !row.lazy).length} widgets scroll without lazy rendering`); }