Both patches came out of this work and both are removed at the maintainer's request. The finding was real - ssh2's WASM heap costs +14.0 MiB RSS on require, and @grpc/grpc-js another 14.6 MiB across 61 files, neither of which a socket-based Docker install ever uses - but patching a dependency means every future dockerode or docker-modem bump fails until someone regenerates the patch, and that recurring cost is not worth roughly 25 MiB. The finding stays on record in SERVER-MEMORY.md because the fix belongs upstream: both packages require a module at file scope while using it in exactly one already-guarded branch, so a PR to each would fix it for every consumer with no patch to maintain here. The two remaining patches, @types/node-unifi and trpc-to-openapi, predate this work and are untouched. allowUnusedPatches was already set on dev and is left alone. Lockfile regenerated: patchedDependencies now lists only those two. Also sharpens the Cache Components entry. The SqliteError was simply an unmigrated build database, which is fixable - but feeding the build a database would be the wrong fix, because serverSetting is per-installation data on the user's volume and a shell prerendered against the build machine's database would bake one install's settings into every other install's first paint. The error was Next.js correctly reporting a request-time read sitting in the shell path.
56 lines
3.3 KiB
Markdown
56 lines
3.3 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 |
|
||
| [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 |
|
||
| Reuse the certificate agent instead of one per request | 1 file | 5.11 → 1.69 ms/request, 60 → 2 TLS handshakes |
|
||
| Stream proxied images instead of buffering them | 2 files | −36% peak RSS under 30 concurrent 2 MiB images |
|
||
| `content-visibility` on media-release cards | 4 lines | −59% image requests, −41% layout objects |
|
||
|
||
The ssh2 / `@grpc/grpc-js` finding (−15 to −18 MiB external) is deliberately **not** in this table: it
|
||
was implemented as two pnpm patches and then reverted, because patching a dependency breaks on every
|
||
upstream bump. The finding is recorded in [SERVER-MEMORY.md](./SERVER-MEMORY.md) and belongs upstream.
|
||
|
||
## 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).
|