Files
homarr/docs/perf
Thomas Camlong 09df2910c5 docs(perf): check the widgets against Vercel's React/Next performance rules
Sourced with the skills CLI - npx skills find "react performance", then
npx skills use affaan-m/ecc@react-performance, an adaptation of vercel-labs
react-best-practices: 70+ rules across 8 categories. Every mechanically
checkable rule was run against the widgets.

Seven already hold, and that is worth recording as much as a finding: no widget
registers a scroll listener, no numeric && can render a literal 0, the nested
array scans that exist are over single-digit collections, React.cache() is
applied, TanStack Query does the deduplication, content-visibility is on the six
lists that need it, and the two components-declared-in-render were already found
and fixed.

One rule this codebase deliberately contradicts: the skill prefers React 19
<Activity> over unmounting for tabs, and this pass did the opposite with
keepMounted={false}. The rule optimises for switching; a dashboard optimises for
mounting. Every board load mounts every widget while a user switches a widget's
inner tabs rarely, so building a hidden monitoring view on every page load buys
a faster switch nobody performs. Where state loss would show - custom-api's user
JSX, the edit modal's forms - the panels stay mounted.

Two candidates declined rather than guessed at, both recorded with reasoning:
the 20+ `data = []` defaults only destabilise while data is undefined, since
react-query returns a stable reference once loaded; and the 7 unlisted Mantine
entry points need two production builds and a first-load-JS diff before anyone
claims they help. @homarr/ui is not eligible at all despite 111 importers,
because Next's barrel optimiser only rewrites pure re-export files.

Also records the process error behind the earlier "no skills CLI" claim: the
entries in ~/.claude/skills are symlinks, which find -type f skips, so the
directory looked empty right after cat had succeeded on a file inside it.
2026-08-11 16:40:27 +02:00
..

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
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.