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