Files
homarr/docs/perf/README.md
T
Thomas Camlong 083a926719 docs(perf): give every one of the 50 widgets a verdict
Completes the sweep: 11 fixed, 8 triaged, 10 read and found clean, 21 verified
bounded. Each verdict records the evidence behind it, because "I read this" and
"its list cannot get long" are different strengths of claim and should not look
alike in a table.

The four widgets the ranking flagged as most likely to hide something all came
back clean, and three were already bounded before I looked - traefik slices to
what its width shows, umami to its own limit option, beszel-alerts to
options.maxHistoryItems. system-resources looked worst of all with 14 unmemoised
maps, until it turned out none of the six chart components it feeds is memoised
either, so a stable array identity would preserve nothing.
2026-08-11 16:33:33 +02:00

51 lines
2.9 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 |
| [WIDGET-AUDIT.md](./WIDGET-AUDIT.md) | All 50 widgets, one at a time, with a verdict and the evidence for each |
| [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).