Split the frontend bundle: 1.44 MB entry chunk down to 246 kB - #6
Merged
Conversation
`build/` was never in .gitignore — `dist/` is there, but vite.config.ts sets `outDir: build/forail`. Nothing had ever committed it until a `git add -A` after a local `npm run build` swept it in, and merging that PR put ten generated assets plus the tsc incremental state into develop. They are pure noise there. The Dockerfile is multi-stage and runs `npm run build` in the builder stage, so the committed copy is never the one that ships. What it does produce is conflicts: any two branches that each ran a build disagree on files nobody edited, which is exactly what happened between #5 and #6. Removed from the index and ignored, along with `*.tsbuildinfo` and the `vite.config.d.ts` that `tsc -b` emits.
…erly
All 91 pages were static imports in App.tsx, so they landed in one entry chunk
that every visitor downloaded and parsed before the first screen appeared --
while a session typically touches a handful of them.
They are loaded on demand now, behind a Suspense boundary inside the layout, so
the chrome stays put while a page arrives. Login and ForcePasswordChange stay
eager: they are the first paint for an unauthenticated or first-login user, and
deferring them would only add a spinner ahead of the thing being waited for.
entry chunk 1.44 MB -> 246 kB (gzip 75 kB)
Splitting the routes exposed a second problem it had been hiding: ServicePortal
came out at 753 kB on its own, from `import * as Icons from 'lucide-react'` --
the whole icon library, because a catalog item names its icon and any of them
can be asked for. Resolved through lucide's dynamic import map instead, one icon
at a time, memoised on the name so lazy() is not called fresh on every render.
ServicePortal 753 kB -> 205 kB (gzip 50 kB)
What remains above 200 kB is vendor code that already loads only with the pages
that use it: recharts, xterm, xyflow.
A budget guards it. `npm run check-bundle` fails on an entry chunk over 400 kB
or any other chunk over 500 kB, and runs in CI after the build -- otherwise the
next eager import silently puts it back. The route test now also asserts which
pages may be eager, so the same mistake fails there with an explanation rather
than only as a size number.
krlex
force-pushed
the
perf/split-the-frontend-bundle
branch
2 times, most recently
from
August 19, 2026 21:29
a7911fe to
fca869e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes L3 from the 2026-08-19 Codex review.
First commit: stop committing the Vite build output
build/was never in.gitignore—dist/is there, butvite.config.tssetsoutDir: build/forail. Nothing had ever committed it until agit add -Aaftera local
npm run buildswept it in, and merging #5 put ten generated assets plusthe
tscincremental state intodevelop. That is what this PR was conflictingon: two branches that each ran a build disagreeing about files nobody edited.
They are noise there regardless — the Dockerfile is multi-stage and runs
npm run buildin the builder stage, so the committed copy is never the one thatships. Removed from the index and ignored, along with
*.tsbuildinfoand thevite.config.d.tsthattsc -bemits. After a full local build,git statusisclean.
The entry chunk
All 91 pages were static imports in
App.tsx, so they landed in one chunk thatevery visitor downloaded and parsed before the first screen appeared — while a
session typically touches a handful of them.
Loaded on demand now, behind a
Suspenseboundary inside the layout so thechrome stays put while a page arrives.
LoginandForcePasswordChangestayeager — they are the first paint for an unauthenticated or first-login user, so
deferring them would only add a spinner ahead of the thing being waited for.
What the split uncovered
Splitting the routes exposed a second problem the single bundle had been hiding:
ServicePortalcame out at 753 kB on its own, fromimport * as Icons from 'lucide-react'— the entire icon library, because acatalog item names its icon and any of them can be asked for.
Resolved through lucide's dynamic import map instead, one icon at a time,
memoised on the name (calling
lazy()during render returns a new component typeeach pass, and React would unmount and remount the icon every time).
What remains above 200 kB is vendor code that already loads only with the pages
using it: recharts, xterm, xyflow.
Keeping it
npm run check-bundlefails on an entry chunk over 400 kB or any other chunkover 500 kB, and runs in CI after the build. The budgets are deliberately
generous against today's numbers — this is a ratchet against regression, not a
target to squeeze against.
The route test also asserts which pages may be eager, so re-adding a static
page import fails there with an explanation rather than only showing up as a
larger number.
Verified
tsc -bclean,npm run lintclean.vitest run: 20 files, 217 tests (214 before — two added here, threearriving with the Stop offering the Jinja2 source type in the survey editor #5 merge).
npm run buildsucceeds with no chunk-size warning, andnpm run check-bundlepasses.