perf(nextjs): stop preloading every route's module graph at startup

Next loads the module graph for all 74 app routes when the server starts. Measured
here: 1,114 files and 24 MiB of source resident before the first request, including
one shared 16 MiB subtree behind the API routes that reaches every integration
client. A dashboard serves a handful of routes per session.

`experimental.preloadEntriesOnStart: false` is Next's documented switch for this.
An earlier round of this work recorded route preloading as "framework behaviour, not
something this codebase sets" - that was wrong, and the findings doc is corrected.

Deterministic per-stage measurements, identical across both runs on each side:

  stage            source loaded      files        heapUsed
  boot idle       24.0 -> 10.6 MiB   1114 -> 706   70.7 -> 44.8  (-37%)
  post-login      27.9 -> 19.3 MiB   1215 -> 884   123.8 -> 109.8
  board loaded    29.0 -> 21.7 MiB   1323 -> 1006  (run-to-run noise)
  multi-tab       29.2 -> 22.9 MiB   1325 -> 1015  139.7 -> 127.8
  post-soak       29.2 -> 22.9 MiB   1325 -> 1015  142.5 -> 127.4

The important part is the last two rows: 310 files and 6.3 MiB of source stay
unloaded even after a full multi-tab session, so this is not merely deferred cost -
routes the session never visits are never paid for at all. Live heap is lower at
every stage, by 26 MiB at idle and 12-15 MiB under load and at rest.

Peak node RSS does not move (350.3 -> 346.1, spreads overlap). V8 sizes its heap
against available headroom, so a smaller live set does not shrink peak RSS; what
improves is idle footprint and how much room is left before the heap cap. Median
board load is +3% with overlapping spreads, so the first-hit cost this trades for
did not show at this workload.

Full E2E suite passes 8/8 against the built image.
This commit is contained in:
Thomas Camlong committed 2026-08-10 09:07:16 +02:00
1 parent 8594cc884f
commit bcea4733b9
1 file changed
+11
+11
View File
@@ -87,6 +87,17 @@ const nextConfig: NextConfig = {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForBuild: true,
useTypeScriptCli: true,
/**
* Next otherwise loads the module graph for every route at startup. Measured here: 74 route
* bundles and 1,114 files, 24 MiB of source, resident before the first request — and one
* shared 16 MiB subtree behind the API routes that reaches every integration. A dashboard
* serves a handful of routes per session, so preloading the rest is pure cost.
*
* The trade is that the first request to a route pays its module load instead of the boot
* paying for all of them. That suits a long-lived self-hosted server, where boot happens
* once and routes warm up as they are used.
*/
preloadEntriesOnStart: false,
},
turbopack: {
root: path.resolve(import.meta.dirname, "../.."),