Why JavaScript size still matters
JavaScript payloads on the web keep growing. HTTP Archive data from mid-2018 put the median transfer size of JavaScript on mobile at roughly 350 KB — and that figure only reflects what crosses the network. After decompression, the actual amount of JavaScript the browser must parse and compile is considerably larger. Compression is irrelevant once the resource arrives: 900 KB of uncompressed JavaScript still costs the parser and compiler 900 KB of work, even if the wire representation is only about 300 KB.
That distinction matters because JavaScript is one of the most expensive resource types to process. Images incur only a relatively trivial decode cost after download; JavaScript must be parsed, compiled, and executed. Byte for byte, that makes it far costlier than other assets. JavaScript engines are improving on this front with techniques like background compilation and bytecode caching, but the primary responsibility for reducing processing cost still rests with developers.
One widely used tactic is code splitting, which partitions application JavaScript into route-specific chunks. That helps, but it doesn't address a separate problem: shipping code that is never used at all. Tree shaking is the technique that targets this issue directly.
How tree shaking works
Tree shaking is a form of dead code elimination, a concept popularized by Rollup but now supported by webpack as well. The mental model treats your application and its dependencies as a tree: each node is a dependency providing distinct functionality. Modern apps pull these dependencies in through static import statements:
import * as utils from "../../utils/utils";
In a young application, nearly every added dependency gets used. As the app matures, dependencies accumulate and some fall out of use without being pruned. The result is that production builds ship a large amount of unused JavaScript. Tree shaking exploits a property of static ES6 module imports: you can pull in only the specific exports you need, rather than an entire module:
import { simpleSort } from "../../utils/utils";
This doesn't change anything in development builds — the whole module is still imported. But in production builds, webpack can be configured to "shake" off ES6 module exports that were never explicitly imported, shrinking those bundles. The rest of this guide shows how to set that up.
Finding tree shaking opportunities
To make this concrete, a sample one-page app — a searchable database of guitar effect pedals — demonstrates the technique. Its code is separated into vendor bundles (Preact and Emotion) and app-specific chunks. The production bundles shown are uglified; the app-specific bundle weighs in at 21.1 KB, but no tree shaking is happening at all.
The first step is to scan for static import statements that pull in entire modules. In the sample's main component file, this import appears:
import * as utils from "../../utils/utils";
This imports everything from the utils module into a namespace called utils. Searching the component file reveals the namespace appears in only three places, and it turns out to be a single function, utils.simpleSort, used to sort search results:
var sortedPedals = utils.simpleSort(pedals, filterState);
The utils module itself is about 1,300 lines of code with many exports. Only one is ever used. The scenario is slightly contrived, but it closely mirrors real optimization opportunities in production applications. This is exactly the kind of unused code tree shaking can remove.
Keep Babel from converting ES6 modules
Babel is invaluable, but if you use @babel/preset-env with its default settings, it may transform ES6 modules into CommonJS modules — the kind you load with require rather than import. Tree shaking is far harder for CommonJS modules, and webpack won't be able to determine what to prune.
The fix is to configure @babel/preset-env explicitly to preserve ES6 modules. That requires adding a small option to your Babel configuration file, whether that is babel.config.js or the Babel section of package.json:
"presets": [["@babel/preset-env", { "modules": false }]]
Setting modules: false stops Babel from rewriting import into require, allowing webpack to examine your dependency tree and remove unused exports.
Side effects and what they mean for shaking
Another important consideration is whether your project's modules have side effects. Side effects occur when a function modifies something outside its own scope. For example:
const fruits = ["apple", "orange"];
function addFruit(fruit) {
fruits.push(fruit);
}
In this snippet, addFruit modifies the fruits array, which is declared outside its scope. That is a side effect of its execution.
Side effects apply to ES6 modules as well, and they matter for tree shaking. A module that consistently maps predictable inputs to predictable outputs without modifying anything outside its own scope is a safe candidate for removal if unused. Such modules are self-contained and truly modular.
Webpack relies on a hint in package.json to know whether a package is side-effect-free:
"sideEffects": false
This tells webpack that a package and its dependencies can be safely dropped when unused. Alternatively, you can list specific files that do have side effects, and anything not listed is considered safe:
"sideEffects": ["./src/some-side-effectful-file.js"]
If you prefer not to touch package.json, the same flag can be set in your webpack config via module.rules.
Import selectively and let webpack prune
With Babel configured to leave ES6 modules intact, the next step is adjusting the import syntax to bring in only the required functions. For the sample app, that means importing simpleSort directly:
import { simpleSort } from "../../utils/utils";
Every instance of utils.simpleSort then becomes a simple simpleSort call. After making these changes, the difference in webpack output is clear. Before shaking, the bundle sizes stay at their original levels. After a successful shake, both the vendor and app bundles shrink, but the main app bundle gains the most — dropping by roughly 60%. That reduction lowers both download time and the amount of JavaScript the browser has to process.
Tree shaking is configurable, so try it
How much you gain from tree shaking depends on your application's dependencies and architecture. If you haven't configured your module bundler for this optimization, there is no harm in trying it and measuring the result. Some applications realize a significant performance win; others see little change. Either way, configuring your build system to enable tree shaking in production builds and importing only what your application actually needs will keep your bundles as small as possible going forward.



