The off-screen work was committed unmeasured; this closes that debt. Measured on two
containers restored from identical data:
image requests 138, 135 -> 22, 22 (-84%)
image bytes 12.27 MiB -> 0.03 MiB
layout objects 11611, 7836 -> 3694, 3705 (-53%)
A 99.8% drop in image bytes is not believable, so it was checked instead of reported. Two
checks were bad instruments before one worked:
- Counting decoded images via getBoundingClientRect() reported 80/80 loaded in BOTH arms.
Reading geometry forces layout, which forces content-visibility subtrees to render, which
triggers the very image loads being tested for. The instrument caused the result.
- Screenshots came back byte-identical in both arms, because this environment shows an
onboarding overlay and media-requests renders blank with unreachable integrations. Good
evidence of no visual difference; useless as evidence that images render.
- Counting img.complete && naturalWidth > 0 reads no geometry and gave the answer: 80/80
decoded in both arms, 138 responses before against 25 after.
So nothing is visually broken. The 80 imgs are integration icons sharing URLs, deduplicated to
~25 unique fetches in both arms; the ~113 avoided requests are other widgets' off-screen
artwork, chiefly media-requests' full-size per-card backdrops, which is also where the 12 MiB
came from.
Stated limit: media-requests renders blank in this environment, so its visible-card behaviour
after the change is unverified. Media-releases is verified at 80/80 decoded.
Replaces the earlier version with what was actually measured, and closes out all 54
findings from the widget audit rather than leaving categories open:
overlay-per-row (12) 5 fixed; 7 are functional tooltips with real labels
index-key (11) all safe - static text splits, or stateless rows from
user-configured JSONPath data with no stable id
nested-component (2) both fixed
inline-style-per-row (29) left alone; a fresh object per row only defeats memoisation
if the child is memoised, and these are not
Measured, deterministic per-widget counts:
mediaReleases 340 comp / 8387 fib -> 170 / 8057
calendar 171 comp / 1195 fib -> 75 / 907
beszelSystemTable 36 comp / 1286 fib -> 18 / 1230
Records two metrics that turned out unusable - Chrome's board-level JSEventListeners read
7905 to 17114 across six runs of identical code, and its Nodes counter includes detached
nodes - and one claim withdrawn: the media-releases tooltip fix measured zero, the win for
that widget came entirely from the shared OverflowBadge fix.
PHASE2-FINDINGS.md had grown past 1,200 lines mixing server memory, client rendering,
methodology and a long list of rejected experiments, which made it unusable as a
reference. Split under docs/perf/:
README.md headline results and the change-by-change ratio table
SERVER-MEMORY.md what the container's RAM is made of, layer by layer
CLIENT-PERFORMANCE.md dashboard cost in Chrome: DOM, fibers, images, paint
REJECTED.md ideas implemented, measured and abandoned, with numbers
METHODOLOGY.md the tools, and the measurement traps that gave wrong answers
PHASE2-FINDINGS.md is now an index so existing links keep working.
METHODOLOGY.md is the part worth keeping: every wrong number in this investigation came
from an instrument rather than the application, and each trap is written down - snapshots
cannot explain peaks, V8 truncates snapshot strings to ~1 KiB, snapshot objects are not
live handles, measuring without a forced GC measures V8's growth, taking a CDP snapshot
added 134 MiB to the process being measured, per-component React attribution silently
returns nothing on a production build, a fixed port can measure a leftover container, and
a server-side change that moves a client-side metric means the workload changed.