Tree shaking sounds like it should be a solved problem by now — bundlers have supported it for years, and it's on by default in most modern build tools. In practice it's still a common source of bloated bundles, mostly because it's easy to write code that quietly defeats it without any error or warning telling you it happened.
What tree shaking actually does
Tree shaking is a form of dead code elimination that removes unused exports from your final JavaScript bundle. Starting from your entry point, the bundler traces which functions and modules are actually reachable and referenced, then drops everything that isn't — code that's imported but never called, entire modules pulled in for one small utility, unused branches of a library.
It relies specifically on the static structure of ES module import/export syntax. Because ES modules declare their imports and exports statically (unlike CommonJS's dynamic require()), a bundler can determine at build time — without running the code — exactly what's used and what isn't, and safely remove the rest.
Why it still matters in 2026
Network speeds have improved substantially over the past several years, which might suggest bundle size matters less than it used to. But JavaScript execution time hasn't kept pace with network improvements — every kilobyte of shipped script still has to be parsed, compiled, and executed on the main thread, and that cost falls hardest on lower-end devices where CPU, not bandwidth, is the bottleneck. A bloated bundle costs real interaction latency even on a fast connection.
What breaks tree shaking silently
This is where most real-world bundle bloat comes from — not bundlers failing to tree-shake, but code patterns that make tree-shaking impossible even though the bundler is configured correctly:
- Importing from a library's root instead of its subpath.
import { debounce } from 'lodash'can pull in far more of the library than you intend if the package isn't properly structured for tree shaking, whereas a more specific import path avoids the problem entirely. Not every library ships tree-shakeable exports, so check before assuming. - Side effects the bundler can't verify are safe to drop. If a module does something at import time beyond declaring exports — mutating a global, registering something, running setup code — the bundler generally can't prove it's safe to remove even if nothing imports from that module, so it gets kept. Marking a package's
sideEffectsfield correctly inpackage.jsonfixes this, but it's easy to leave unset or incorrectly set tofalsewhen the package actually has side effects. - CommonJS interop. Mixing CommonJS
require()/module.exportsinto a codebase built around ES modules can break static analysis, since CommonJS exports are dynamic and can't always be traced at build time the way ES module exports can. - Re-export barrel files. A common pattern —
export * from './module'files that aggregate exports from many files into one — can accidentally defeat tree shaking in some bundler configurations if the barrel file itself gets treated as having side effects, pulling in everything behind it even if only one specific export is actually used.
How to verify it's actually working
Don't just assume tree shaking is happening because you're using a modern bundler in production mode. Check your actual bundle with a visualizer — tools like webpack-bundle-analyzer, Rollup's visualizer plugin, or Vite's built-in bundle analysis — and look specifically for whole libraries or modules appearing in the bundle when you only use a small piece of them. That's the clearest sign tree shaking silently failed somewhere in the dependency chain.
The actual performance cost, quantified
It's worth being specific about why this matters, since "bloated bundles are bad" is easy to nod along with and then deprioritize. Roughly every 100KB of JavaScript costs about 1 second of interaction delay on median mobile hardware — a 500KB bundle might download in 250ms on a decent 4G connection, but take 3-5 seconds to parse and execute on a mid-range Android phone from a few years back. That parse-and-execute cost, not the download itself, is why bundle size still matters even as network speeds have improved: the bottleneck moved from bandwidth to CPU, and tree shaking is one of the few levers that directly reduces CPU cost rather than just download time.
Current data on where sites actually land: HTTP Archive measurements show median JavaScript payload per page sits around 520KB compressed (1.8MB uncompressed) on mobile, with the 90th percentile reaching 1.2MB compressed. Against that backdrop, the widely cited target of keeping an initial JavaScript bundle under 200KB gzipped is aspirational for a lot of real sites — but it's also the level at which sites consistently see INP (Interaction to Next Paint) scores under 200ms at the 75th percentile, which is the current Core Web Vitals threshold for a "good" score. INP specifically is where most sites are struggling in 2026 — over 60% of mobile origins exceed the 200ms threshold — and unused code sitting in your bundle because tree shaking silently failed is one of the more fixable contributors to that number, compared to deeper architectural issues.
The practical takeaway
Tree shaking works well by default across webpack, Rollup, Vite, and esbuild in production builds — the tooling side of this problem is largely solved. What isn't solved is code and dependency choices that quietly defeat it: side-effect-heavy modules, CommonJS interop, and poorly structured barrel exports. Periodically checking your actual bundle output with a visualizer, rather than trusting that tree shaking "just works," is the only reliable way to catch when it hasn't. Given the roughly linear relationship between shipped kilobytes and interaction delay on median mobile hardware, treating a bundle-analyzer check as a routine step before shipping a dependency change — the same way a test suite run is routine — is a low-cost habit that catches regressions before they reach users, rather than after a Core Web Vitals dashboard flags a INP regression weeks later with no clear culprit.
Sources: webpack.js.org, generalistprogrammer.com, dev.to/playfulprogramming
Keep reading
Get new posts as they publish
No spam — just the next post, straight to your inbox.