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.
49 lines
2.6 KiB
Markdown
49 lines
2.6 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 |
|
||
|
||
## 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).
|