Files
homarr/docs/perf/README.md
T
Thomas Camlong c73b139dbd docs(perf): split the findings into per-subject markdown files
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.
2026-08-10 19:08:20 +02:00

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