Files
homarr/docs/perf/README.md
T
Thomas Camlong 8f94ea47e3 docs(perf): add a handoff with the unverified item, the remaining lever, and the clean-PR set
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.
2026-08-10 19:12:28 +02:00

50 lines
2.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).