Code splitting and lazy loading

Play this lesson

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

menu.js
import { openEditor }  from './editor.js';import { startGarden }  from './garden.js'; editorBtn.onclick = () => {  openEditor();};

Kai wants the 900 KB editor to download only when someone taps the editor button. Which lines have to change?

  1. Lines 1 and 2
  2. Lines 3 and 4
  3. 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

Quick check

Why put PixiJS in a separate vendor chunk?

  1. It changes less often than our code, so players keep their cached copy when we ship updates
  2. It makes PixiJS run faster
  3. 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".

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