The branch was 12 commits behind dev and GitHub reported it CONFLICTING, which is
why the real CI workflow had not run since 2026-08-10 - a conflicting PR has no
mergeable ref, so pull_request-triggered workflows are skipped. Only the
lightweight checks (semantic title, CodeQL, CodeRabbit) were still reporting,
which made the PR look green while nothing was actually being tested.
Three conflicts, all of them this branch's barrel-diet work meeting dev's new
What's Up Docker integration. In every case the branch's structure is kept and
WUD is absorbed into it rather than the reverse:
integrations/base/creator.ts keep the lazy loader map; add wud as
() => import(...) rather than dev's static import,
which would have undone the lazy creator
integrations/src/index.ts keep the slim barrel; re-export WudStats and
WudContainerUpdate as `export type`, which is
erased at build and so adds no runtime module
widgets/src/index.tsx stays deleted - this was the barrel that put
every widget in the initial client graph
Registering WUD in the slimmed structures then needed three more entries that the
merge could not infer: base/kind-map.ts (type-only kind -> class map),
widgets/src/import.ts (type-only WidgetImports map, both the import and the
member). Each was found by typecheck rather than guessed at.
Verified: 37/37 packages typecheck, 1866/1881 unit tests pass. The 3 failures are
home-assistant and pi-hole, both testcontainers-based; both pass 11/11 in
isolation and fail only under 150-file parallel load with the container's payload
half-initialised.
Homarr performance investigation
Measured work on Homarr's memory and rendering cost, split by subject. Every figure here comes from
a run of scripts/benchmarks/stress-restore.mts against a restored real backup — 4 boards, 51
widgets, 11 integrations, driven through an actual login — not from a synthetic or empty container.
| document | subject |
|---|---|
| SERVER-MEMORY.md | What the container's RAM is made of, and the changes that reduced it |
| CLIENT-PERFORMANCE.md | Dashboard rendering cost in Chrome: DOM, fibers, images, paint |
| WIDGET-AUDIT.md | All 50 widgets, one at a time, with a verdict and the evidence for each |
| REJECTED.md | Ideas that were implemented and measured, then abandoned — with the numbers |
| METHODOLOGY.md | The tools, and the measurement traps that produced wrong answers |
| NEXT-STEPS.md | Handoff: what is unverified, what to do next, and the set for a clean PR |
Headline result
Server RAM, merge-base 06724cfeb vs this branch, two alternated runs per side, reported as cgroup
anonymous memory because that is what counts against a container limit and cannot be reclaimed:
| stage | before | after | change |
|---|---|---|---|
| peak | 376.6 MiB | 279.3 MiB | −26% |
| boot idle | 130.5 MiB | 62.4 MiB | −52% |
| after sign-in | 247.4 MiB | 142.9 MiB | −42% |
| dashboard loaded | 293.4 MiB | 170.4 MiB | −42% |
| 4 tabs, 2 boards | 366.5 MiB | 258.2 MiB | −30% |
| after 2 min idle soak | 323.7 MiB | 200.8 MiB | −38% |
No before/after range overlaps at any stage. The cost is +18% CPU (solid, non-overlapping).
The changes that did it, by ratio
| change | size | effect |
|---|---|---|
--max-semi-space-size=4 |
1 line | −19% idle, −21% peak |
preloadEntriesOnStart: false |
1 line | −52% anon at idle, 310 fewer files even at full load |
| Umami: stop fetching 100k raw events per refresh | 1 file | −26% peak, −93% transient buffers |
serverExternalPackages (mysql2/pg + 8 more) |
1 block | −21 MiB idle |
| ssh2's 16 MiB WASM heap (docker-modem patch) | 1 patch | −15 to −18 MiB external |
content-visibility on media-release cards |
4 lines | −59% image requests, −41% layout objects |
What is left, and why it is hard
One lever has real headroom and it pays twice. Roughly 41 MiB of retained bundle source and ~38 MiB of V8 compiler zone memory both scale with how much JavaScript the server loads and compiles, and the largest single contributor is a 16.10 MiB subtree reached by every API route — the tRPC router importing every integration eagerly. Reducing that reduces both.
Everything else has been measured and closed: see REJECTED.md.