From bcea4733b9b14c58d04c9550623d520d1d82d9f7 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 10 Aug 2026 09:07:16 +0200 Subject: [PATCH] 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. --- apps/nextjs/next.config.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index ab735d7d6..0b6a2a466 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -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, "../.."),