Search

Why JavaScript Bundles Get So Big (and How to Shrink Them)

The short answer

Quick answer: A JavaScript bundle is the file (or files) a build tool produces by combining your code with every library it depends on. Bundles grow because of large and duplicated dependencies, importing whole libraries to use one function, shipping code for every page on the first page, compatibility code for old browsers, and a build that cannot remove what is unused. Size matters because JavaScript must be downloaded, parsed, compiled and executed on the user's device, which is slow on ordinary phones. The main remedies are code splitting (load only what the current page needs), tree shaking (drop unused code), smaller dependencies, and compression.

Why bundlers exist

Modern applications are written as hundreds or thousands of small modules that import each other, plus packages from npm. Browsers could not originally load modules at all, and even now fetching thousands of separate files is slow.

A bundler such as webpack, Rollup, esbuild, Vite or Parcel:

  1. Starts from an entry file.
  2. Follows every import to build a dependency graph.
  3. Transforms the code (TypeScript, JSX, newer syntax).
  4. Combines the result into a small number of files.
  5. Minifies and optimises them.

HTTP/2 and HTTP/3 made many small requests cheaper, but did not remove the need for bundling: deep chains of imports still cause slow, sequential loading. See why HTTP/2 and HTTP/3 exist.

Why size matters more for JavaScript

A 300 KB image and a 300 KB script are not equally costly. After downloading, JavaScript must be:

  1. Decompressed.
  2. Parsed and compiled.
  3. Executed.

All of this happens on the main thread, the same one that handles rendering and user input. While it is busy, the page cannot respond. See how browsers render a web page.

A fast laptop hides the cost. A mid-range phone may take several times longer to process the same script. Heavy JavaScript directly harms the Core Web Vitals, particularly responsiveness to input, and with them user experience and search performance.

Where the weight comes from

Dependencies, and their dependencies

Installing one package can pull in dozens of others. Your own code is frequently a small fraction of the bundle.

Importing everything to use a little

import _ from "lodash";           // may bring in the whole library
const result = _.debounce(fn, 200);

import debounce from "lodash-es/debounce";   // brings in one function

Libraries that cannot be tree-shaken

Older packages published in the CommonJS format (require / module.exports) are hard for bundlers to analyse, so the whole package is often included.

Duplicates

Two of your dependencies each need a different version of a third. Both versions end up in the bundle.

Polyfills and transpiled syntax

Supporting old browsers means adding polyfills for missing features and converting modern syntax into longer, older equivalents. If your audience uses current browsers, much of this is dead weight.

Everything on the first page

A single bundle containing the code for every route, including the admin panel, the settings page and the chart library only used in reports, makes every visitor download it all.

Other common culprits

  • Large date and time, charting, rich text editor and icon libraries.
  • Locale data for every language.
  • Large JSON files or images embedded directly in the code.
  • Development-only code accidentally left in production builds.
  • CSS-in-JS runtimes.

How to shrink it

1. Measure first

Guessing is unreliable. Use a bundle analyser (webpack-bundle-analyzer, rollup-plugin-visualizer, or source-map-explorer) to see a map of exactly what is inside. The largest boxes are usually a surprise.

Also check the Coverage tab in browser developer tools, which shows how much of the downloaded JavaScript actually ran.

2. Code splitting

Split the bundle into chunks and load each only when needed. The web.dev guide to code splitting explains the technique.

By route. Each page gets its own chunk. Most frameworks do this automatically.

By component or feature. Load heavy parts on demand with a dynamic import():

// Loaded only when the user opens the chart
button.addEventListener("click", async () => {
  const { renderChart } = await import("./chart.js");
  renderChart(data);
});
// React
const Editor = React.lazy(() => import("./Editor"));

Vendor splitting. Put rarely changing libraries in a separate chunk so browsers can keep it cached across your deployments.

3. Tree shaking

Tree shaking removes exported code that nothing imports. It depends on ES modules (import / export), whose structure is fixed and can be analysed without running the code.

To make it work:

  • Use ES module versions of libraries.
  • Import named exports, not the whole package.
  • Check that packages declare themselves free of side effects ("sideEffects": false in package.json), so the bundler knows unused files can be dropped.

4. Choose lighter dependencies

  • Check a package's size before adding it.
  • Prefer small, focused libraries, or modern ones designed for tree shaking.
  • Use what the platform now provides: fetch, Intl for dates and numbers, structuredClone, CSS for animations.
  • Ask whether you need the dependency at all. A ten-line function may replace a package.

5. Minify and compress

  • Minification removes whitespace and comments and shortens names. Production builds do this by default.
  • Compression on the server shrinks the transfer. Brotli generally beats gzip for text. JavaScript typically compresses to a fraction of its original size.

Compression reduces download time only. The browser still has to parse and run the full, uncompressed code.

6. Target modern browsers

Set your build's browser targets to match your real audience. Dropping support for long-obsolete browsers removes polyfills and lets the output use shorter modern syntax.

7. Load third-party scripts carefully

Analytics, chat widgets, advertising and tag managers are often the heaviest scripts on a page, and they sit outside your bundle. Audit them regularly, load them with async or defer, and delay non-essential ones until after the page is usable.

8. Send less JavaScript in the first place

The most effective reduction is not shipping code to the browser at all. Rendering on the server, server components and islands architecture keep much of the work, and the code, off the client. See server-side vs client-side rendering.

9. Cache well

Give built files a content hash in their name (app.3f9a1c.js) and serve them with a long cache lifetime, from a CDN. Returning visitors then download only the chunks that changed.

Keeping it small

Bundle size creeps up one reasonable-looking addition at a time.

  • Set a performance budget: a maximum size for the initial JavaScript.
  • Check it in your pipeline, and fail the build or flag the pull request when it is exceeded. See how CI/CD pipelines work.
  • Review the size impact of every new dependency.

Pitfalls

  • Over-splitting. Hundreds of tiny chunks cause many requests and delay. Aim for sensible groupings.
  • Loading waterfalls. A lazily loaded chunk that then loads another, which loads another. Preload what you know will be needed.
  • Lazy-loading content that is visible immediately, which delays it for no gain.
  • Optimising the wrong thing. Measure on a real mid-range phone or with throttling, not on a developer laptop.

Frequently asked questions

What is a JavaScript bundle?

One or more files produced by a build tool that combine your application code and its dependencies for delivery to the browser.

What is tree shaking?

A build step that removes code which is exported but never imported, so it is not shipped to users.

What is code splitting?

Dividing a bundle into smaller chunks that are loaded on demand, so each page downloads only the code it needs.

What is a good bundle size?

There is no single number, but smaller is better, especially for the JavaScript needed on first load. Set a budget, measure on modest devices, and keep the initial payload as lean as the page allows.

Conclusion

Bundles grow because adding code is easy and its cost is invisible on a fast machine. That cost is paid by users on slower devices, in download, parsing and execution time. Look inside the bundle, split it by route, let tree shaking remove what is unused, question every dependency, and set a budget so the size stays under control.

Related articles

Sources and further reading

Usama Muneer

Usama Muneer

Coder, Blogger, Tech Speaker & Web Technologies Enthusiast. Passionate about working on open-source Programming languages & Tools while utilizing my Product Development skills.

Your experience on this site will be improved by allowing cookies Cookie Policy