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.
Per-widget attribution of the dashboard found one widget responsible for 44% of the
board's DOM: mediaReleases at 2,951 nodes and 8,387 fibers, including 1,935 Mantine
Box components, against a board total of 9,293 nodes.
The cause is that it maps over every release the integrations return, with no cap.
Measured on a real board: the tile is 539 px tall and its content is 7,206 px, so of
80 rendered cards only 7 are visible. The other 73 cost DOM, fibers, and - most
expensively - a poster and a CSS backdrop image each, all fetched and decoded for
pixels nobody can see. Decoded bitmaps are far larger in memory than their files.
Fixed with content-visibility: auto plus contain-intrinsic-size on each card, so the
browser skips layout, paint, raster and background-image fetch for off-screen cards,
and loading="lazy" on the poster. Nothing leaves the DOM, so scrolling and in-page
search are unchanged, and the intrinsic size keeps the scrollbar from jumping.
Measured over three repeats per side on identical restored data:
image requests 135, 135, 135 -> 55, 55, 56 (-59%, deterministic)
image bytes 12.27 MiB -> 8.41 MiB (-31%)
layout objects 8346, 7815, 7815 -> 5117, 4696, 4636 (-41%)
Two things this does NOT show, stated because a single run suggested otherwise:
- JS heap did not clearly improve. Across four runs it was 46-88 MiB before against
74-86 MiB after; the before spread is 2x, so no claim either way. This is expected:
decoded image memory lives outside JSHeapUsedSize, so the actual saving from 81
fewer image fetches is invisible to that metric.
- Task duration and time-to-settled overlap between arms and support no conclusion.
Adds the tools this needed: measure-widget-cost.mts (per-widget DOM nodes, fibers and
heaviest components), measure-board-paint.mts (image requests, bytes, style/layout/task
time, layout objects), measure-widget-overdraw.mjs (rendered versus visible rows).