Reference guide · html-css-js · Published 2026-08-15 · 4 min read
Reducing JavaScript bundle size
Reduce JavaScript bundle size: measure, code split, tree shake, remove dead code, ship production builds and inspect source maps.
- ·Measure first
- ·Cut the bundle
- ·Toolchain defaults
Measure first, cut second
Every byte of JavaScript must be downloaded, parsed, compiled and executed, and unlike CSS most of that work happens on the main thread. The heavy JavaScript slow site article shows the effect in the Interaction to Next Paint metric. Before touching anything, measure so the effort lands where the size lives.
The way to see the real number: open the built site in an incognito window, put the Network panel on the JS filter, hard reload, and read the transferred size per script. Then open the coverage tab in the DevTools, reload, and record the unused ratio for each file. Most apps discover that one utility module is shipped everywhere.
Cutting the bundle
Code splitting
Code splitting turns one giant chunk into several chunks that load on demand. The route for a page that uses a chart library is a textbook case: the chart only matters on that route, so the library enters its own chunk loaded when the route renders, not at first paint.
const Chart = () => import("./chart.js");
Tree shaking
Tree shaking removes exports that are defined but never imported. It only works when the build is in production mode and the module is written with static import and export (not module.exports). A library imported as import { debounce } from "lodash" lets the bundler drop the rest. Imported as import * as _ from "lodash", the whole library enters the bundle, because tree shaking cannot see which parts are unused.
Remove dead and duplicate code
- Delete feature-flag code that has shipped for months and is no longer toggleable.
- Replace a full-library import with the specific entry point it ships.
- Check for duplicate copies of the same dependency at different versions; a version range bump that drags in two copies doubles the bytes.
Production builds and source maps
Development builds include assertions, hot reload and verbose error messages, easy to ship by accident. Set the build to production, then confirm minification and tree shaking across the whole dependency graph. Keep source maps in the build output but serve them only with the authenticated source map server, or strip them from prod entirely. The 20 to 50 percent saving is not the effect of stripping maps in a normal build; it is the gap of a dev-mode bundle actually shipped, because dev builds carry assertions, hot reload and source maps the visitor never needs. The browser's console footer warning about missing maps exists to catch exactly that mistake.
The bundle-size table
| Move | Typical saving | Warning |
|---|---|---|
| Production build, not dev | 20 to 50 percent | Saving is dropping the dev build, not maps alone |
| Tree shaking | 10 to 60 percent, depends on imports | ES modules only |
| Code splitting on routes | Worst route shrinks hard | Chunk duplication if shared libraries split |
| Removing a dead library | Exact size of the file | Long tail of references first |
| Replacing full import | Size of the unused share | Source compatibility |
Verification
Rerun the total transfer size with the same repeat measurement, then the Interaction to Next paint number from the lab, then watch the page in the network panel while switching routes to prove the on-demand chunks load. A bundle weight that went down but a TTFB that went up is a chunk boundary in the wrong place, see the core web vitals article for the wholesale view.
Prevention
Bundles creep. Set the CI alert when the transfer size exceeds a threshold, add the duplicate-dependency check, and write a one-line story for every new dependency: what it imports, and how much tree shaking cuts it. When the pain returns, the render blocking resources pass and the slow page article are the next two places to look.