perf(media-releases): stop mounting a Popover per card for tooltips that never show
Attributing listener-attaching components per widget found the same anti-pattern the
shared board context menu already fixed once, still present in list widgets:
interact nodes fibers kind / which components
340 2951 8387 mediaReleases
UnstyledButton x90 TooltipFloating x80 Popover x80
PopoverDropdown x80 PopoverTarget x10
171 251 1195 calendar
UnstyledButton x45 Popover x42 PopoverDropdown x42
PopoverTarget x42
46 264 898 downloads
Tooltip x30
Mantine mounts a TooltipFloating's Popover machinery even when the tooltip is disabled, so
wrapping every card unconditionally produced 80 TooltipFloating, 80 Popover and 80
PopoverDropdown components - 240 of this widget's 340 listener-attaching components - for
tooltips that only show on hover, on cards that are 91% off-screen.
Deciding before rendering means a card with no description, or with the option off, mounts
none of it. Behaviour is unchanged for cards that do have a description.
Also extends measure-widget-cost.mts to count these components per widget, which is what
found it. The board's 8,165 event listeners were never attributable before; now they are.
Not yet measured: the listener reduction depends on how many releases carry a description,
which is integration data. The mechanism is certain (the components are simply not created)
but the size of the win on a given board is not, and calendar's 42 Popovers and downloads'
30 Tooltips are untouched. Tracked in docs/perf/NEXT-STEPS.md.
This commit is contained in:
2 files changed
+154
-127
No files matched your search
@@ -57,128 +57,138 @@ const Item = ({ item, options }: ItemProps) => {
|
||||
const t = useI18n();
|
||||
const length = formatLength(item.length, item.type, t);
|
||||
|
||||
return (
|
||||
<TooltipFloating
|
||||
label={item.description}
|
||||
w={300}
|
||||
multiline
|
||||
disabled={item.description === undefined || item.description.trim() === "" || !options.showDescriptionTooltip}
|
||||
>
|
||||
<UnstyledButton
|
||||
component="a"
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
pos="relative"
|
||||
p={options.layout === "poster" ? 0 : 4}
|
||||
style={{
|
||||
// This widget maps over every release the integrations return. Measured on a real board:
|
||||
// 80 cards rendered into a 539 px tile whose content is 7,206 px tall, so 73 of them are
|
||||
// scrolled out of view — along with their backdrop and poster images, which the browser
|
||||
// otherwise fetches, decodes and rasterises for pixels nobody can see.
|
||||
//
|
||||
// content-visibility lets the browser skip layout, paint and raster for a card that is
|
||||
// off-screen, and contain-intrinsic-size gives it a placeholder height so the scrollbar
|
||||
// does not jump as cards enter and leave. Nothing is removed from the DOM, so scrolling
|
||||
// and in-page search behave exactly as before.
|
||||
contentVisibility: "auto",
|
||||
containIntrinsicSize: "auto 88px",
|
||||
}}
|
||||
>
|
||||
{options.layout === "backdrop" && (
|
||||
<Box
|
||||
w="100%"
|
||||
h="100%"
|
||||
pos="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
style={{
|
||||
backgroundImage: `url(${item.imageUrls.backdrop})`,
|
||||
borderRadius: 8,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
opacity: 0.2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Group justify="space-between" h="100%" wrap="nowrap">
|
||||
<Group align="start" wrap="nowrap" style={{ zIndex: 0 }}>
|
||||
{options.layout === "poster" && (
|
||||
// Only a handful of the rendered cards are on screen; loading the rest's posters
|
||||
// eagerly costs a request and a decoded bitmap each for no visible benefit.
|
||||
<Image w={60} src={item.imageUrls.poster} alt={item.title} loading="lazy" />
|
||||
)}
|
||||
<Stack gap={4}>
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" fw="bold" lineClamp={2}>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.subtitle !== undefined && (
|
||||
<Text size="sm" lineClamp={1}>
|
||||
{item.subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<Group gap={6} style={{ rowGap: 0 }}>
|
||||
<Info icon={IconCalendar} label={formatReleaseDate(item.releaseDate, locale)} />
|
||||
{length !== undefined && (
|
||||
<>
|
||||
<InfoDivider />
|
||||
<Info icon={length.type === "duration" ? IconClock : IconBook} label={length.label} />
|
||||
</>
|
||||
)}
|
||||
{item.producer !== undefined && (
|
||||
<>
|
||||
<InfoDivider />
|
||||
<Info label={item.producer} />
|
||||
</>
|
||||
)}
|
||||
{item.rating !== undefined && (
|
||||
<>
|
||||
<InfoDivider />
|
||||
<Info icon={IconStarFilled} label={item.rating} />
|
||||
</>
|
||||
)}
|
||||
{item.price !== undefined && (
|
||||
<>
|
||||
<InfoDivider />
|
||||
<Info label={`$${item.price.toFixed(2)}`} />
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
{item.tags.length > 0 && (
|
||||
<OverflowBadge
|
||||
size="xs"
|
||||
groupGap={4}
|
||||
data={item.tags}
|
||||
overflowCount={3}
|
||||
disablePopover
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
{(options.showType || options.showSource) && (
|
||||
<Stack justify="space-between" align="end" h="100%" style={{ zIndex: 0 }}>
|
||||
{options.showType && (
|
||||
<Badge
|
||||
w="max-content"
|
||||
size="xs"
|
||||
color={mediaTypeConfigurations[item.type].color}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{item.type}
|
||||
</Badge>
|
||||
)}
|
||||
// Mantine mounts a TooltipFloating's Popover machinery even when the tooltip is disabled, so
|
||||
// wrapping unconditionally cost a Popover and a PopoverDropdown per card. Measured on a real
|
||||
// board: 80 cards produced 80 TooltipFloating, 80 Popover and 80 PopoverDropdown components —
|
||||
// 240 of the widget's 340 listener-attaching components — for tooltips that mostly never show,
|
||||
// on cards that are 91% off-screen. Deciding here means a card without a description, or with
|
||||
// the option off, renders none of it.
|
||||
const showDescriptionTooltip =
|
||||
options.showDescriptionTooltip && item.description !== undefined && item.description.trim() !== "";
|
||||
|
||||
{options.showSource && (
|
||||
<Avatar size="sm" radius="xl" src={getIconUrl(item.integration.kind)} alt={item.integration.name} />
|
||||
const card = (
|
||||
<UnstyledButton
|
||||
component="a"
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
pos="relative"
|
||||
p={options.layout === "poster" ? 0 : 4}
|
||||
style={{
|
||||
// This widget maps over every release the integrations return. Measured on a real board:
|
||||
// 80 cards rendered into a 539 px tile whose content is 7,206 px tall, so 73 of them are
|
||||
// scrolled out of view — along with their backdrop and poster images, which the browser
|
||||
// otherwise fetches, decodes and rasterises for pixels nobody can see.
|
||||
//
|
||||
// content-visibility lets the browser skip layout, paint and raster for a card that is
|
||||
// off-screen, and contain-intrinsic-size gives it a placeholder height so the scrollbar
|
||||
// does not jump as cards enter and leave. Nothing is removed from the DOM, so scrolling
|
||||
// and in-page search behave exactly as before.
|
||||
contentVisibility: "auto",
|
||||
containIntrinsicSize: "auto 88px",
|
||||
}}
|
||||
>
|
||||
{options.layout === "backdrop" && (
|
||||
<Box
|
||||
w="100%"
|
||||
h="100%"
|
||||
pos="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
style={{
|
||||
backgroundImage: `url(${item.imageUrls.backdrop})`,
|
||||
borderRadius: 8,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
opacity: 0.2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Group justify="space-between" h="100%" wrap="nowrap">
|
||||
<Group align="start" wrap="nowrap" style={{ zIndex: 0 }}>
|
||||
{options.layout === "poster" && (
|
||||
// Only a handful of the rendered cards are on screen; loading the rest's posters
|
||||
// eagerly costs a request and a decoded bitmap each for no visible benefit.
|
||||
<Image w={60} src={item.imageUrls.poster} alt={item.title} loading="lazy" />
|
||||
)}
|
||||
<Stack gap={4}>
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" fw="bold" lineClamp={2}>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.subtitle !== undefined && (
|
||||
<Text size="sm" lineClamp={1}>
|
||||
{item.subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
<Group gap={6} style={{ rowGap: 0 }}>
|
||||
<Info icon={IconCalendar} label={formatReleaseDate(item.releaseDate, locale)} />
|
||||
{length !== undefined && (
|
||||
<>
|
||||
<InfoDivider />
|
||||
<Info icon={length.type === "duration" ? IconClock : IconBook} label={length.label} />
|
||||
</>
|
||||
)}
|
||||
{item.producer !== undefined && (
|
||||
<>
|
||||
<InfoDivider />
|
||||
<Info label={item.producer} />
|
||||
</>
|
||||
)}
|
||||
{item.rating !== undefined && (
|
||||
<>
|
||||
<InfoDivider />
|
||||
<Info icon={IconStarFilled} label={item.rating} />
|
||||
</>
|
||||
)}
|
||||
{item.price !== undefined && (
|
||||
<>
|
||||
<InfoDivider />
|
||||
<Info label={`$${item.price.toFixed(2)}`} />
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
{item.tags.length > 0 && (
|
||||
<OverflowBadge
|
||||
size="xs"
|
||||
groupGap={4}
|
||||
data={item.tags}
|
||||
overflowCount={3}
|
||||
disablePopover
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
{(options.showType || options.showSource) && (
|
||||
<Stack justify="space-between" align="end" h="100%" style={{ zIndex: 0 }}>
|
||||
{options.showType && (
|
||||
<Badge
|
||||
w="max-content"
|
||||
size="xs"
|
||||
color={mediaTypeConfigurations[item.type].color}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{item.type}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{options.showSource && (
|
||||
<Avatar size="sm" radius="xl" src={getIconUrl(item.integration.kind)} alt={item.integration.name} />
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
);
|
||||
|
||||
if (!showDescriptionTooltip) return card;
|
||||
|
||||
return (
|
||||
<TooltipFloating label={item.description} w={300} multiline>
|
||||
{card}
|
||||
</TooltipFloating>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ const perWidget = await page.evaluate(() => {
|
||||
if (!key) return { fibers: 0, top: [] as [string, number][] };
|
||||
const root = (element as unknown as Record<string, unknown>)[key] as Record<string, unknown>;
|
||||
const counts = new Map<string, number>();
|
||||
const interactive = new Map<string, number>();
|
||||
let fibers = 0;
|
||||
const seen = new Set<unknown>();
|
||||
const stack: (Record<string, unknown> | null | undefined)[] = [root];
|
||||
@@ -74,21 +75,35 @@ const perWidget = await page.evaluate(() => {
|
||||
? (type.displayName ?? type.name)!
|
||||
: null;
|
||||
if (name) counts.set(name, (counts.get(name) ?? 0) + 1);
|
||||
// Components that attach their own DOM listeners. A per-item Tooltip or Popover is the
|
||||
// same anti-pattern the shared board context menu fixed, and listener count is the only
|
||||
// metric that exposes it.
|
||||
if (name && /Tooltip|Popover|Menu|UnstyledButton|ActionIcon|Anchor|Button|Checkbox|Input/.test(name)) {
|
||||
interactive.set(name, (interactive.get(name) ?? 0) + 1);
|
||||
}
|
||||
stack.push(fiber.child as never, fiber.sibling as never);
|
||||
}
|
||||
return { fibers, top: [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4) };
|
||||
return {
|
||||
fibers,
|
||||
top: [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4),
|
||||
interactive: [...interactive.entries()].sort((a, b) => b[1] - a[1]),
|
||||
};
|
||||
};
|
||||
|
||||
const byKind = new Map<string, { count: number; nodes: number; fibers: number; components: Map<string, number> }>();
|
||||
const byKind = new Map<
|
||||
string,
|
||||
{ count: number; nodes: number; fibers: number; components: Map<string, number>; interactive: Map<string, number> }
|
||||
>();
|
||||
for (const item of document.querySelectorAll('[data-type="item"]')) {
|
||||
const kind = item.getAttribute("data-kind") ?? "(unknown)";
|
||||
const nodes = item.querySelectorAll("*").length;
|
||||
const stats = fiberStats(item);
|
||||
const acc = byKind.get(kind) ?? { count: 0, nodes: 0, fibers: 0, components: new Map() };
|
||||
const acc = byKind.get(kind) ?? { count: 0, nodes: 0, fibers: 0, components: new Map(), interactive: new Map() };
|
||||
acc.count++;
|
||||
acc.nodes += nodes;
|
||||
acc.fibers += stats.fibers;
|
||||
for (const [name, n] of stats.top) acc.components.set(name, (acc.components.get(name) ?? 0) + n);
|
||||
for (const [name, n] of stats.interactive ?? []) acc.interactive.set(name, (acc.interactive.get(name) ?? 0) + n);
|
||||
byKind.set(kind, acc);
|
||||
}
|
||||
|
||||
@@ -104,8 +119,10 @@ const perWidget = await page.evaluate(() => {
|
||||
nodesEach: Math.round(acc.nodes / acc.count),
|
||||
fibersEach: Math.round(acc.fibers / acc.count),
|
||||
components: [...acc.components.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3),
|
||||
interactive: [...acc.interactive.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5),
|
||||
interactiveTotal: [...acc.interactive.values()].reduce((sum, n) => sum + n, 0),
|
||||
}))
|
||||
.sort((a, b) => b.nodes - a.nodes),
|
||||
.sort((a, b) => b.interactiveTotal - a.interactiveTotal),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -125,14 +142,14 @@ console.log(` event listeners ${metric("JSEventListeners")}`);
|
||||
console.log(` documents ${metric("Documents")}`);
|
||||
console.log(` widgets on board ${perWidget.itemCount}`);
|
||||
|
||||
console.log(`\n=== cost per widget kind, heaviest first ===`);
|
||||
console.log(`${"nodes".padStart(7)} ${"each".padStart(6)} ${"fibers".padStart(7)} ${"each".padStart(6)} ${"n".padStart(3)} kind / heaviest components`);
|
||||
console.log(`\n=== listener-attaching components per widget kind, heaviest first ===`);
|
||||
console.log(`${"interact".padStart(9)} ${"nodes".padStart(7)} ${"fibers".padStart(7)} ${"n".padStart(3)} kind / which components`);
|
||||
for (const kind of perWidget.kinds) {
|
||||
console.log(
|
||||
`${String(kind.nodes).padStart(7)} ${String(kind.nodesEach).padStart(6)} ${String(kind.fibers).padStart(7)} ${String(kind.fibersEach).padStart(6)} ${String(kind.count).padStart(3)} ${kind.kind}`,
|
||||
`${String(kind.interactiveTotal).padStart(9)} ${String(kind.nodes).padStart(7)} ${String(kind.fibers).padStart(7)} ${String(kind.count).padStart(3)} ${kind.kind}`,
|
||||
);
|
||||
if (kind.components.length) {
|
||||
console.log(`${" ".repeat(33)}${kind.components.map(([name, n]) => `${name}×${n}`).join(" ")}`);
|
||||
if (kind.interactive.length) {
|
||||
console.log(`${" ".repeat(29)}${kind.interactive.map(([name, n]) => `${name}×${n}`).join(" ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user