Blog

Blog · Performance & Optimization

How to Reduce JavaScript Bundle Size in 2026: 14 Ways

Cut your JavaScript bundle — measure with an analyzer, code-split by route, tree-shake, replace heavy dependencies, enable Brotli, set a realistic browserslist and enforce a budget in CI — each with why and a code example.

XCODX Team · 6 min read

Every kilobyte of JavaScript has to be downloaded, parsed, compiled and executed — and the parse/compile cost is what really hurts on mid-range phones. A smaller bundle size means a faster load, better INP and a better Core Web Vitals score. Here are 14 ways to reduce your JavaScript bundle in 2026, each with the reasoning and a code example.

1. Measure with a bundle analyzer first

You can’t optimize a bundle you can’t see. A visual analyzer shows exactly which modules and dependencies take the most space, so you fix the real problem instead of guessing. source-map-explorer works with any bundler; rollup-plugin-visualizer is great for Vite/Rollup.

# any bundler, from source maps
npx source-map-explorer dist/assets/*.js

# Vite / Rollup treemap
npm i -D rollup-plugin-visualizer

2. Code-split by route

Shipping the whole app up front is the most common cause of a bloated initial bundle. Splitting each route into its own dynamically-imported chunk means users download only the page they’re on. This is usually the single biggest reduction to *initial* JavaScript.

const Dashboard = lazy(() => import("./routes/Dashboard"));
const Settings  = lazy(() => import("./routes/Settings"));
// each route becomes its own chunk, loaded on navigation

3. Tree-shake unused code

Tree shaking removes exports you never import. It only works on ES modules (static import/export), so use ESM everywhere and let your bundler run in production mode. Mark your package "sideEffects": false (with a CSS allowlist) so the bundler can drop untouched modules entirely — see our tree shaking guide.

// package.json — enable aggressive tree shaking, but keep CSS
{
  "sideEffects": ["**/*.css", "**/*.scss"]
}

4. Find dead code and unused dependencies with Knip

Codebases accumulate unused files, exports and dependencies that still get bundled or ship as dead weight. Knip scans your project and reports exactly what’s unused, so you can delete it with confidence. Run it in CI to stop new dead code creeping back in.

npx knip
# → unused files, unused exports, and unused/mis-listed dependencies

5. Replace or trim heavy dependencies

One oversized library can dwarf your own code. Check candidates on Bundlephobia before adding them, and swap heavy ones for lean alternatives: date-fns or dayjs (a few KB) instead of moment, native Intl/fetch instead of large helpers, and modular imports instead of a whole utility kitchen-sink. Removing one heavy dependency often beats a dozen micro-optimizations.

// ❌ moment.js is large and not tree-shakeable
import moment from "moment";

// ✅ dayjs is ~2KB with a similar API
import dayjs from "dayjs";

6. Import only what you use — avoid barrel imports

Importing from a package’s barrel (index.js) can pull in far more than you use if the library isn’t perfectly tree-shakeable. Import the specific submodule/function instead. This matters most for large utility and icon libraries, where a single named import can otherwise drag in the whole set.

// ❌ may pull the whole library
import { debounce } from "lodash";
// ✅ import just the function
import debounce from "lodash/debounce";
// (or use lodash-es, which tree-shakes)

7. Lazy-load heavy components and libraries

A rich text editor, a charting library, a map or a PDF viewer can each be larger than your entire app shell. Load them with a dynamic import() only when the user actually opens that feature, keeping them out of the initial bundle entirely.

chartTab.addEventListener("click", async () => {
  const { renderChart } = await import("./charts");   // its own chunk
  renderChart(data);
});

8. Enable Brotli (or gzip) compression

Text assets compress extremely well over the wire — Brotli typically beats gzip by ~15–20% on JavaScript. Serve .js/.css/.html with Brotli where supported and gzip as a fallback; most CDNs (Cloudflare, Netlify, Vercel) do this automatically. This shrinks transfer size without touching your code.

# nginx: prefer pre-compressed Brotli, fall back to gzip
brotli on;
brotli_static on;
gzip on;
gzip_static on;

9. Set a realistic browserslist

Transpiling to old JavaScript and injecting polyfills for browsers nobody uses bloats every bundle. A modern browserslist (e.g. the current widely-used baseline) lets your bundler emit smaller, modern syntax and skip needless polyfills. Check your real analytics before choosing targets.

// package.json
{
  "browserslist": ["> 0.5%", "last 2 versions", "not dead", "not op_mini all"]
}

10. Minify in production

Minification strips whitespace, shortens names and removes dead code, cutting JavaScript substantially. Modern bundlers do this by default in production builds (Vite/Rollup use esbuild/terser; esbuild is extremely fast). Just make sure you’re building in production mode — a dev build ships unminified and much larger.

# Vite production build: minified + tree-shaken by default
vite build
# verify NODE_ENV=production and you're not shipping the dev bundle

11. Deduplicate dependencies

Two versions of the same library (React, lodash) bundled together is pure waste and can cause bugs. Deduplicate your lockfile and, if needed, force a single version so it’s only bundled once. Check the analyzer treemap for suspicious duplicate module names.

# npm
npm dedupe
# or pin one copy in package.json
{ "overrides": { "react": "19.x" } }

12. Split vendor code into cacheable chunks

Your dependencies change far less often than your app code. Splitting stable vendor libraries into their own chunk means a code change to your app doesn’t bust the (large) vendor cache, so returning visitors re-download less. Configure manual chunks in your bundler.

// vite.config.js
export default {
  build: { rollupOptions: { output: {
    manualChunks: { vendor: ["react", "react-dom"] },
  } } },
};

13. Use modern, fast build tooling

Modern bundlers produce smaller output with better defaults and far faster builds. Vite (moving to the Rust-based Rolldown bundler) and esbuild give aggressive tree shaking, minification and code splitting out of the box. Keep tooling current — bundler improvements are free size wins you get just by upgrading.

# scaffold a modern, optimized build
npm create vite@latest my-app
# tree shaking, code splitting and minification on by default

14. Enforce a bundle budget in CI

Bundle size regresses one innocent import at a time. A size budget checked on every pull request fails the build when a change pushes a bundle over a threshold, so bloat is caught before it ships. size-limit is a simple, popular choice.

// .size-limit.json
[{ "path": "dist/assets/index-*.js", "limit": "170 KB" }]
// package.json: "size": "size-limit"  — run it in CI on every PR

Bundle size checklist

  • Measure with an analyzer before changing anything.
  • Code-split by route; lazy-load heavy components and libraries.
  • Tree-shake (ESM + sideEffects); delete dead code with Knip.
  • Replace heavy dependencies; import specific submodules, not barrels.
  • Enable Brotli/gzip; set a realistic browserslist; minify in production.
  • Deduplicate deps; split stable vendor code into its own chunk.
  • Use modern tooling (Vite/Rolldown); enforce a size budget in CI.

Frequently asked questions

What is a good JavaScript bundle size?
Aim to keep initial (above-the-fold) JavaScript small — a common target is under ~150–200 KB compressed for the critical path — because parse and execution time on mid-range phones scales with bytes. There’s no universal number; the right move is to measure, set a budget for your app, and enforce it in CI so it can’t regress.
What’s the fastest way to cut bundle size?
Usually two things: code-split by route so users download only the current page, and remove or replace one or two oversized dependencies (for example momentdayjs). Measure with a bundle analyzer first so you spend effort on the biggest offenders.
Does tree shaking reduce bundle size automatically?
Only if the conditions are met: use ES modules (not CommonJS), build in production mode, and mark your package "sideEffects": false (allowlisting CSS). Then the bundler can drop exports and modules you never import. Verify it worked with an analyzer — misconfiguration silently disables it.
How much does compression help?
A lot on the wire. Text assets compress well, and Brotli typically beats gzip by ~15–20% on JavaScript. It shrinks transfer size without changing your code, and most CDNs enable it automatically — but it’s transfer-only; the browser still parses the full uncompressed size, so shipping less code still matters.

Experiment with imports and see bundle impact live in XCODX Studio — npm packages run in the browser, no install. See also our Tree Shaking Guide, Code Splitting Guide and How to Speed Up JavaScript.