Files
homarr/docs/perf/README.md
T
Thomas Camlong 2ebfe659b2 revert(perf): drop the dockerode and docker-modem pnpm patches
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.
2026-08-14 11:16:55 +02:00

3.3 KiB
Raw Blame History

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 What the container's RAM is made of, and the changes that reduced it
CLIENT-PERFORMANCE.md Dashboard rendering cost in Chrome: DOM, fibers, images, paint
WIDGET-AUDIT.md All 50 widgets, one at a time, with a verdict and the evidence for each
REJECTED.md Ideas that were implemented and measured, then abandoned — with the numbers
METHODOLOGY.md The tools, and the measurement traps that produced wrong answers
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 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.