Code splitting and lazy loading
Everyone downloads the editor
Remy finds that Shard Garden's web build is one 2.4 MB main.js. Inside it is the whole level editor, which most players never open.
JavaScript costs more than its download. The phone must also parse it (read it), compile it (turn it into instructions) and run it, all on the main thread. Less code at startup means a faster first screen and quicker taps. Code splitting cuts the code into chunks that load when needed.
How import() works
A normal import at the top of a file loads before anything runs. import('./editor.js'), written with brackets, loads the file only when that line runs. It returns a promise, a value that arrives later, holding everything the file exports.
Each module runs only once. A second import() of the same file reuses it with no new download. The download can fail on a bad connection, so the game should catch the error and offer to try again.
Bug hunt
1import { openEditor }2 from './editor.js';3import { startGarden }4 from './garden.js';5 6editorBtn.onclick = () => {7 openEditor();8};
Kai wants the 900 KB editor to download only when someone taps the editor button. Which lines have to change?
- Lines 1 and 2
- Lines 3 and 4
- Line 6
Show the answer
Lines 1 and 2
Lines 1 and 2 are a static import, so the editor loads with the menu. Inside the click handler, const { openEditor } = await import('./editor.js') fetches it on the first tap.
Tree shaking
Tree shaking is the bundler removing code nothing uses. Bundlers like Rollup or webpack read the import and export lines to see which functions are really used, and drop the rest.
That is why MDN says to use import() only when necessary: static imports are easier for tools to analyse and shake. Rule of thumb: static imports for what the first screen needs, import() for big parts that only some players open.
Chunks by screen, and a vendor chunk
- By screenOne chunk each for the title screen, the garden, the shop and the level editor. The first visit loads only the first screen's.
- Vendor chunkLibraries from other people, like PixiJS, go in their own chunk. They change less often than our code, so players keep them cached across our updates.
Quick check
Why put PixiJS in a separate vendor chunk?
- It changes less often than our code, so players keep their cached copy when we ship updates
- It makes PixiJS run faster
- It makes the game smaller in total
Show the answer
It changes less often than our code, so players keep their cached copy when we ship updates
With hashed file names, the vendor chunk keeps the same name until PixiJS itself changes. Our daily fixes only change our own chunks.
Don't split into a waterfall
Too many tiny chunks cause a waterfall: the browser downloads one module, only then discovers its imports, and fetches them one level at a time.
<link rel="modulepreload" href="garden.js"> fetches a module early, then also parses and compiles it, ready to run. Browsers may skip a module's own imports, so list each important one. While players sit on the title screen, the garden chunk is already on its way.
Quick check
link rel="modulepreload" only downloads the file, exactly like rel="preload".
- True
- False
Show the answer
False
preload only downloads and stores the file. modulepreload also parses and compiles the module and puts it in the module map, ready to run.
Keep it split
Splitting once isn't enough, because code creeps back. A CI check on every pull request can compare each chunk's size against the studio's performance budget, and fail when the first-screen chunk grows past it.
Bolt says: Next: fonts, a small download that can hide every word on screen.
Fact-checked 11 October 2026
- web.dev: Reduce JavaScript payloads with code splitting web.dev
- MDN: import() developer.mozilla.org
- MDN: Tree shaking developer.mozilla.org
- MDN: rel=modulepreload developer.mozilla.org