Mantine's Tabs keeps inactive panels mounted by default. On Mantine 9 with
React 19 that uses <Activity>, which preserves state and unmounts effects but
still builds the entire subtree - so a two-tab widget pays for both while
showing one.
health-monitoring both the system and the cluster monitoring view were built
media-missing both tabs built their full card list, each card with a
poster and a blurred backdrop
Two other Tabs users are deliberately left mounted: custom-api panels can hold
user component state, and the widget edit modal would reset half-filled forms.
media-missing also rendered every card into a scrolling tile with no lazy
loading, the same shape as media-releases. Its card height is fixed per density,
so contain-intrinsic-size is exact rather than a guess.
calendar fell back to https://placehold.co/400x400 when a poster failed to
load, so a self-hosted dashboard made a third-party request - once per failing
event - to draw a grey square, and showed nothing at all on a host without
internet access. Same grey square, inline.
The rss feed handler logged the whole serialised feed entry through a raw
console.debug, which no log level could suppress, and reported a match count
that was really the regex capture-group count.
Adds scripts/benchmarks/widget-inventory.mjs and docs/perf/WIDGET-AUDIT.md, a
verdict for all 50 widgets.
150 lines
6.3 KiB
JavaScript
150 lines
6.3 KiB
JavaScript
/**
|
|
* 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, /<img[\s>]|<Image[\s>]|MaskedOrNormalImage|backgroundImage/g),
|
|
lazy: /loading="lazy"|offscreenRowStyle|contentVisibility/.test(all),
|
|
scrollArea: /<ScrollArea/.test(all),
|
|
memo: count(all, /\bmemo\(/g),
|
|
useMemo: count(all, /useMemo\(/g),
|
|
subscription: /useSubscription|onData|\.subscribe\(/.test(all),
|
|
interval: lookupInterval(widget),
|
|
hasComponent: componentSource.length > 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`);
|
|
}
|