Records where this stopped so it can be picked up cleanly:
1. c526dd2b7 (media-requests content-visibility) is typechecked but NOT measured - the
only performance claim on this branch not A/B'd before commit. Verify with
measure-board-paint.mts, three repeats; revert if image requests do not fall.
2. The one lever with headroom left is the tRPC router's eager integration imports - a
16.10 MiB subtree of 460 files reached by every API route, which pays twice because
both the ~41 MiB of retained source and the ~38 MiB of V8 compiler zones scale with
how much JavaScript the server compiles. Every allocator and V8-flag avenue is closed.
3. Deferred with prizes attached: ASCII-escaping bundles (9.45 MiB, needs its own PR),
lazy mysql2/pg (21.4 MiB, structurally blocked), beszelSystemStats chart SVG.
4. The proposed set for the clean PR, six changes producing -26% peak anon and -52% at
idle for +18% CPU - with the gRPC patch flagged as hygiene rather than a measured win,
and the +30% board-load figure flagged as not established.
50 lines
2.7 KiB
Markdown
50 lines
2.7 KiB
Markdown
# 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](./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 |
|
||
| [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 |
|
||
|
||
## 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](./REJECTED.md).
|