From 083a92671987e6f9b9ea77c7ab2eba0105e89425 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 11 Aug 2026 16:33:33 +0200 Subject: [PATCH] docs(perf): give every one of the 50 widgets a verdict Completes the sweep: 11 fixed, 8 triaged, 10 read and found clean, 21 verified bounded. Each verdict records the evidence behind it, because "I read this" and "its list cannot get long" are different strengths of claim and should not look alike in a table. The four widgets the ranking flagged as most likely to hide something all came back clean, and three were already bounded before I looked - traefik slices to what its width shows, umami to its own limit option, beszel-alerts to options.maxHistoryItems. system-resources looked worst of all with 14 unmemoised maps, until it turned out none of the six chart components it feeds is memoised either, so a stable array identity would preserve nothing. --- docs/perf/README.md | 1 + docs/perf/WIDGET-AUDIT.md | 90 ++++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/docs/perf/README.md b/docs/perf/README.md index b19272c41..48dd2c0e2 100644 --- a/docs/perf/README.md +++ b/docs/perf/README.md @@ -8,6 +8,7 @@ widgets, 11 integrations, driven through an actual login — not from a syntheti | --- | --- | | [SERVER-MEMORY.md](./SERVER-MEMORY.md) | What the container's RAM is made of, and the changes that reduced it | | [CLIENT-PERFORMANCE.md](./CLIENT-PERFORMANCE.md) | Dashboard rendering cost in Chrome: DOM, fibers, images, paint | +| [WIDGET-AUDIT.md](./WIDGET-AUDIT.md) | All 50 widgets, one at a time, with a verdict and the evidence for each | | [REJECTED.md](./REJECTED.md) | Ideas that were implemented and measured, then abandoned — with the numbers | | [METHODOLOGY.md](./METHODOLOGY.md) | The tools, and the measurement traps that produced wrong answers | | [NEXT-STEPS.md](./NEXT-STEPS.md) | Handoff: what is unverified, what to do next, and the set for a clean PR | diff --git a/docs/perf/WIDGET-AUDIT.md b/docs/perf/WIDGET-AUDIT.md index da7c670e8..a692c3f4e 100644 --- a/docs/perf/WIDGET-AUDIT.md +++ b/docs/perf/WIDGET-AUDIT.md @@ -57,67 +57,81 @@ without internet access, which is a normal way to run Homarr. ## The checklist -Verdicts: **fixed** — changed here or in earlier passes · **clean** — read, nothing worth changing · -**triaged** — findings examined and deliberately declined, reason recorded · **signals** — signals are -benign, not read line by line. +Verdicts, and the evidence behind each — the distinction matters, because "I read it" and "its list +cannot get long" are different strengths of claim: + +- **fixed** — changed here or in an earlier pass +- **clean (read)** — the component was read; nothing worth changing +- **clean (bounded)** — list length verified bounded by a widget option, by viewport width, or by the + remote service (active streams, configured instances, physical disks), *and* the row is cheap: + no image, no overlay. Not a line-by-line read +- **triaged** — a finding was examined and deliberately declined, reason recorded | ✓ | # | widget | LOC | verdict | note | | --- | --- | --- | --- | --- | --- | -| [x] | 1 | `downloads` | 1525 | clean | top of the ranking, and the flag was wrong: it memoises data, sorting, columns, sizes and stats. Renders via `DataTable`; list is bounded by active downloads | +| [x] | 1 | `downloads` | 1525 | clean (read) | top of the ranking, and the flag was wrong: it memoises data, sorting, columns, sizes and stats. Renders via `DataTable`; list is bounded by active downloads | | [x] | 2 | `media-transcoding` | 521 | triaged | 2 overlays per row, both functional tooltips with real labels | | [x] | 3 | `dns-hole` | 761 | triaged | icons render through `MaskedOrNormalImage`, whose default branch is a CSS mask, so `loading="lazy"` does not apply | | [x] | 4 | `patchmon` | 1154 | triaged | per-row tooltip is functional | | [x] | 5 | `beszel` | 1186 | fixed | disk-usage mounted a `HoverCard` per row for systems reporting no filesystems | | [x] | 6 | `health-monitoring` | 1250 | **fixed** | hidden tab panel built its whole tree | | [x] | 7 | `media-requests` | 542 | fixed | `content-visibility` + `loading="lazy"` on both images | -| [x] | 8 | `tracearr` | 445 | clean | poster is a CSS background per stream card, and the list is active streams — a handful | +| [x] | 8 | `tracearr` | 445 | clean (read) | poster is a CSS background per stream card, and the list is active streams — a handful | | [x] | 9 | `custom-api` | 1014 | triaged | 6 index keys, all safe: stateless read-only rows from user JSONPath data with no stable id | | [x] | 10 | `firewall` | 334 | triaged | 4 inline-style-per-row, children not memoised so nothing is defeated | | [x] | 11 | `releases` | 817 | fixed | `OverflowBadge` mounted a `Popover` per row even with nothing to overflow | -| [ ] | 12 | `coolify` | 752 | signals | scrolls without lazy; icon is a fixed constant URL, not per-row data | -| [ ] | 13 | `notebook` | 1189 | signals | rich-text editor — self-measuring content, where `content-visibility` is actively wrong | -| [ ] | 14 | `umami` | 595 | signals | top-list could be long; worth a closer look | -| [ ] | 15 | `traefik` | 289 | signals | router/service list could be long | +| [x] | 12 | `coolify` | 752 | clean (bounded) | one map over configured instances; the icon is a fixed constant URL, not per-row data | +| [x] | 13 | `notebook` | 1189 | clean (read) | rich-text editor. Deliberately **not** given `content-visibility`: it measures itself, and skipping its layout would be wrong | +| [x] | 14 | `umami` | 595 | clean (read) | the top-list is bounded by the widget's own `limit` option and each row is three `` — containment would cost more than it saves | +| [x] | 15 | `traefik` | 289 | clean (read) | already bounded: `.slice(0, getEntryPointLimit(width))` | | [x] | 16 | `weather` | 481 | triaged | per-row overlay is a functional tooltip | -| [ ] | 17 | `system-resources` | 568 | signals | 14 maps, no memoisation | +| [x] | 17 | `system-resources` | 568 | clean (read) | the 14 maps build chart data series, not DOM rows, and none of the six chart components is memoised — so memoising the arrays would defeat nothing. Its data changes every tick anyway | | [x] | 18 | `app` | 356 | triaged | icon is a masked image; single icon per widget, not a list | -| [ ] | 19 | `beszel-alerts` | 288 | signals | alert list could be long | -| [ ] | 20 | `beszel-system-stats` | 220 | signals | — | +| [x] | 19 | `beszel-alerts` | 288 | clean (read) | fully memoised — input, name map, alerts and history — and history is sliced to `options.maxHistoryItems` | +| [x] | 20 | `beszel-system-stats` | 220 | clean (bounded) | one row per configured system | | [x] | 21 | `media-missing` | 261 | **fixed** | tabs, lazy poster, off-screen cards | -| [ ] | 22 | `media-server` | 457 | signals | 5s refetch | +| [x] | 22 | `media-server` | 457 | clean (read) | the flat session list is explicitly memoised on `currentStreams`, and is bounded by active streams | | [x] | 23 | `bookmarks` | 545 | triaged | 5 image sites, all masked images | -| [ ] | 24 | `immich` | 301 | signals | carousel shows one photo at a time | -| [ ] | 25 | `ups` | 271 | signals | one device | +| [x] | 24 | `immich` | 301 | clean (bounded) | carousel shows one photo at a time | +| [x] | 25 | `ups` | 271 | clean (bounded) | one row per UPS device | | [x] | 26 | `docker` | 590 | fixed | uses `OverflowBadge` | -| [ ] | 27 | `indexer-manager` | 150 | signals | — | -| [ ] | 28 | `vpn` | 227 | signals | — | +| [x] | 27 | `indexer-manager` | 150 | clean (bounded) | one row per configured indexer | +| [x] | 28 | `vpn` | 227 | clean (bounded) | one row per tunnel | | [x] | 29 | `calendar` | 507 | **fixed** | `HoverCard` per day cell (126 of 171 components), lazy images, off-screen event rows, and the `placehold.co` fallback | -| [ ] | 30 | `speedtest-tracker` | 707 | signals | — | -| [ ] | 31 | `system-disks` | 196 | signals | — | -| [ ] | 32 | `minecraft` | 91 | signals | one server icon | -| [ ] | 33 | `paperless-ngx` | 255 | signals | — | -| [ ] | 34 | `uptime-kuma` | 268 | signals | — | -| [ ] | 35 | `iframe` | 148 | signals | — | -| [ ] | 36 | `stocks` | 146 | signals | — | -| [ ] | 37 | `beszel-system-grid` | 496 | signals | — | -| [ ] | 38 | `audio-stats` | 305 | signals | — | -| [ ] | 39 | `bazarr` | 141 | signals | — | +| [x] | 30 | `speedtest-tracker` | 707 | clean (bounded) | no list map in the component | +| [x] | 31 | `system-disks` | 196 | clean (bounded) | one row per physical disk | +| [x] | 32 | `minecraft` | 91 | clean (bounded) | one server icon | +| [x] | 33 | `paperless-ngx` | 255 | clean (bounded) | statistic rows, fixed set | +| [x] | 34 | `uptime-kuma` | 268 | clean (bounded) | one row per monitor | +| [x] | 35 | `iframe` | 148 | clean (bounded) | maps are over option lists, not remote data | +| [x] | 36 | `stocks` | 146 | clean (bounded) | one row per configured symbol | +| [x] | 37 | `beszel-system-grid` | 496 | clean (bounded) | already slices its input | +| [x] | 38 | `audio-stats` | 305 | clean (bounded) | no list map in the component | +| [x] | 39 | `bazarr` | 141 | clean (bounded) | one row per configured instance | | [x] | 40 | `beszel-system-table` | 488 | fixed | declared `PercentCell` inside the render body, so three cell subtrees per system remounted every second | -| [ ] | 41 | `clock` | 213 | signals | ticks, but that is what it is for | +| [x] | 41 | `clock` | 213 | clean (read) | re-renders once a second by design; no list | | [x] | 42 | `media-releases` | 275 | fixed | 80 cards into a 539 px tile; `OverflowBadge`; lazy posters | -| [ ] | 43 | `network-controller` | 231 | signals | — | -| [ ] | 44 | `timetable` | 110 | signals | — | -| [ ] | 45 | `anchor-note` | 340 | signals | editor content | +| [x] | 43 | `network-controller` | 231 | clean (bounded) | one row per device | +| [x] | 44 | `timetable` | 110 | clean (bounded) | one map over the configured schedule | +| [x] | 45 | `anchor-note` | 340 | clean (read) | editor content, no list; self-measuring like `notebook` | | [x] | 46 | `notifications` | 113 | fixed | off-screen rows | | [x] | 47 | `rssFeed` | 131 | fixed | off-screen rows; payload and handler checked | -| [ ] | 48 | `smart-home` | 235 | signals | — | -| [ ] | 49 | `archive-team-warrior` | 105 | signals | — | -| [ ] | 50 | `video` | 144 | signals | — | +| [x] | 48 | `smart-home` | 235 | clean (bounded) | one entity per widget | +| [x] | 49 | `archive-team-warrior` | 105 | clean (bounded) | no list map | +| [x] | 50 | `video` | 144 | clean (bounded) | a single video element | -**22 of 50 read in depth. 26 remain at signal level** — their signals are benign, but that is a -prediction, not a reading. The unread ones with a real chance of something are `umami` (long top -lists), `traefik` and `beszel-alerts` (list length driven by remote data), and `system-resources` -(14 maps). +**All 50 have a verdict.** 11 fixed, 8 triaged, 10 read and found clean, 21 verified bounded. + +The four the ranking suggested were most likely to hide something all came back clean, and three of +them were *already* bounded before I looked: `traefik` slices to what its width can show, `umami` to +its own `limit` option, `beszel-alerts` to `options.maxHistoryItems`. `system-resources` was the +interesting one — its 14 unmemoised maps look bad until you notice none of the six chart components it +feeds is memoised, so there is nothing for a stable array identity to preserve. + +The 21 "bounded" verdicts are the weakest claim here. They rest on the list length being fixed by +configuration or by physical reality — disks, tunnels, configured instances — plus a cheap row with no +image and no overlay. That is sound reasoning, not a reading, and a widget whose data source changes +shape could invalidate it. ## Investigated and declined, with reasons