Concept · Building apps

Tree shaking and code splitting

Ship only the code that’s used, and only when it’s needed.

3 mintree shakingdynamic importlazy loadingbundle size

Every kilobyte of JavaScript has to be downloaded, parsed, compiled and executed before it does anything. On a mid-range phone, that work often costs more than the download. Bundlers like Vite, Rollup, esbuild and webpack have two main tools to reduce it.

Tree shaking: drop unused code

ES modules have static import/export statements, so the bundler can see at build time exactly which exports are used and leave out the rest.

js
import { debounce } from 'lodash-es'   // only debounce (and what it uses) is bundled
import _ from 'lodash'                 // CommonJS: the whole library, ~70 KB

Tree shaking fails when:

  • The package is CommonJS (require/module.exports), because its exports are dynamic.
  • A module has side effects at import time (patching globals, registering things). The bundler has to keep it. Libraries declare "sideEffects": false in package.json to say they're safe to drop.
  • You import a barrel file (index.js re-exporting everything). Some setups end up pulling in the whole barrel.

Code splitting: load code when it's needed

A dynamic import() tells the bundler to put a module and its dependencies in a separate chunk, fetched only when that code runs:

jsx
const Chart = lazy(() => import('./Chart'))      // separate chunk

<Suspense fallback={<Skeleton />}>
  {showChart && <Chart data={data} />}
</Suspense>

Split along these lines:

  • Routes. Most frameworks do this automatically: each page loads only its own code.
  • Heavy components behind an interaction: editors, charts, maps, modals.
  • Large libraries used in one place, like a PDF renderer or a syntax highlighter.

Don't split everything into tiny pieces. Each chunk is a request, and a chain of chunks that each discover the next one creates a waterfall. Preload the chunks you know you'll need next, for example on link hover.

Measuring

  • Use a bundle visualizer (rollup-plugin-visualizer, vite-bundle-visualizer) to see what's large and why.
  • Check the Coverage panel in Chrome DevTools to see how much of the shipped code actually runs on load.
  • Set a size budget in CI so regressions fail the build instead of piling up.

Smaller bundles mean faster hydration and fewer long tasks on the main thread, which you'll see in INP and LCP.

Learn it in depth