diff --git a/packages/widgets/src/media-releases/component.tsx b/packages/widgets/src/media-releases/component.tsx index 268264ac9..d5a693a97 100644 --- a/packages/widgets/src/media-releases/component.tsx +++ b/packages/widgets/src/media-releases/component.tsx @@ -57,128 +57,138 @@ const Item = ({ item, options }: ItemProps) => { const t = useI18n(); const length = formatLength(item.length, item.type, t); - return ( - - - {options.layout === "backdrop" && ( - - )} - - - {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. - {item.title} - )} - - - - {item.title} - - {item.subtitle !== undefined && ( - - {item.subtitle} - - )} - - - - {length !== undefined && ( - <> - - - - )} - {item.producer !== undefined && ( - <> - - - - )} - {item.rating !== undefined && ( - <> - - - - )} - {item.price !== undefined && ( - <> - - - - )} - - {item.tags.length > 0 && ( - - )} - - - {(options.showType || options.showSource) && ( - - {options.showType && ( - - {item.type} - - )} + // 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 && ( - + const card = ( + + {options.layout === "backdrop" && ( + + )} + + + {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. + {item.title} + )} + + + + {item.title} + + {item.subtitle !== undefined && ( + + {item.subtitle} + )} - )} + + + {length !== undefined && ( + <> + + + + )} + {item.producer !== undefined && ( + <> + + + + )} + {item.rating !== undefined && ( + <> + + + + )} + {item.price !== undefined && ( + <> + + + + )} + + {item.tags.length > 0 && ( + + )} + - + {(options.showType || options.showSource) && ( + + {options.showType && ( + + {item.type} + + )} + + {options.showSource && ( + + )} + + )} + + + ); + + if (!showDescriptionTooltip) return card; + + return ( + + {card} ); }; diff --git a/scripts/benchmarks/measure-widget-cost.mts b/scripts/benchmarks/measure-widget-cost.mts index 0f227c1e6..e072f0bf1 100644 --- a/scripts/benchmarks/measure-widget-cost.mts +++ b/scripts/benchmarks/measure-widget-cost.mts @@ -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)[key] as Record; const counts = new Map(); + const interactive = new Map(); let fibers = 0; const seen = new Set(); const stack: (Record | 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 }>(); + const byKind = new Map< + string, + { count: number; nodes: number; fibers: number; components: Map; interactive: Map } + >(); 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(" ")}`); } }