2HAASStart a project
All posts

Tech Tips

Tree shaking: why your bundle still ships unused code

Tree shaking removes code you never import, but only when the bundler can prove it is unused. ES modules, the sideEffects flag and your import style decide what goes.

The 2HAAS team2 min read

Tree shaking is the bundler's way of leaving out code you never use. Webpack, Rollup, Vite and esbuild all do it. Yet many projects that have it switched on still ship whole libraries for the sake of one function. The reason is that tree shaking only works when the bundler can prove the code is unused.

It depends on ES modules

import and export are static. The bundler can read a file and know exactly what it uses without running it. CommonJS is different: require() is a function call that can happen anywhere, with any argument, so the bundler has to keep everything.

Two things follow from that:

  • A library published only as CommonJS will not shake well.
  • Your own build must not convert modules to CommonJS before bundling. With Babel, keep modules: false in @babel/preset-env. With TypeScript, use "module": "esnext".

The Lodash example, done properly

This is the case most tutorials get wrong.

// Ships the whole library
import _ from "lodash";

// Looks selective, but lodash is CommonJS, so most setups still ship all of it
import { debounce } from "lodash";

// Ships only debounce
import debounce from "lodash/debounce";

// Also fine: the ES module build of the same library
import { debounce } from "lodash-es";

Named imports only help when the package is written as ES modules.

Tell the bundler what is safe to drop

A module can do work just by being imported, such as registering a polyfill or injecting CSS. The bundler cannot always tell, so it plays safe and keeps the file. The sideEffects field in package.json removes the doubt.

{
  "sideEffects": ["*.css"]
}

This says: everything except CSS files can be dropped when nothing is imported from it. Use false if no file has side effects at all.

What quietly blocks tree shaking

  • Code that runs at the top of a module. If importing a file changes something global, the file stays.
  • Large classes. A bundler removes unused exports, not unused methods. One class with forty methods ships all forty.
  • Dynamic access. utils[name]() forces the bundler to keep every member of utils.
  • Wildcard re-exports of CommonJS code. One non-shakeable file can hold a whole barrel file in the bundle.

Check that it worked

Open the bundle in an analyzer, or simply search the production output for the name of a function you know you never call. If it is there, something above is keeping it alive.

Tree shaking is one part of a smaller bundle. The rest is covered in how to reduce JavaScript bundle size.

Keep reading

  1. Tech Tips2 min read

    JavaScript performance: keep the main thread free

    A page can load fast and still feel slow. Batch DOM updates, debounce noisy events and break up long tasks to keep the interface responsive.

  2. Tech Tips2 min read

    Async error handling in JavaScript: try/catch done right

    Unhandled promise rejections crash Node.js services and leave browser apps quietly broken. Catch async errors with try/catch, .catch and a global safety net.

  3. Tech Tips2 min read

    WebP and AVIF: modern image formats with a fallback

    WebP and AVIF produce much smaller files than JPEG and PNG at the same visual quality. How to convert your images and serve them with the picture element.

Tell us what’s in the way.

A few lines is enough. You get a candid yes, a candid no, or a sharper question.

  • Working software in week four
  • A reply in one business day
  • Handover is the deliverable

Prefer email? [email protected]

The problem, who it is for, and what is in the way right now.

One business day to a real reply. No sales sequence, no call required.