The Cost of JavaScript: Bundle Size Optimization
Why a kilobyte of JavaScript costs more than a kilobyte of anything else, and how tree shaking, code splitting, dependency audits, and CI budgets keep bundles lean.
Published on • August 10, 2026
AI Assistant

Bytes of JavaScript cost more than bytes of any other resource type. An image is downloaded and decoded. JavaScript must be downloaded, parsed, compiled, and executed — and while it executes, it blocks the main thread, freezing user interaction. A 200KB JavaScript bundle and a 200KB image have wildly different performance implications. In 2026, the median mobile site ships over 500KB of compressed JavaScript, and bundle size is the primary driver of poor INP scores.
In this post, you will learn how to measure what’s actually in your bundle, the mechanics of tree shaking and code splitting, how to audit dependencies, and how to enforce bundle budgets in CI so size never creeps back.
Measure before you optimize
You can’t optimize what you can’t measure. Start with bundle analysis tools that visualize exactly which modules and dependencies contribute the most weight:
webpack-bundle-analyzer/rollup-plugin-visualizer— treemap visualization of your bundle. The biggest squares are your problems.- Bundlephobia — check any npm package’s minified and gzipped size plus dependency count before you install it.
size-limit— measures real cost in bytes and milliseconds on CI, with a--whyflag that names the offending module.
// rollup-plugin-visualizer in a Vite config
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({ open: true, gzipSize: true, brotliSize: true }),
],
};
Tree shaking: eliminate dead code
Tree shaking is static dead-code elimination — the bundler drops exports that are never imported. It only works when two conditions hold: you use ES module syntax (import/export, not CommonJS require), and the module graph has no hidden side effects that block pruning. Webpack also uses the sideEffects field in package.json to skip entire files during pruning.
The classic win: import specific functions instead of whole libraries.
// Before: pulls in the entire library (~70KB)
import _ from 'lodash';
const groups = _.groupBy(items, 'type');
// After: tree-shakeable named import, only what you use (~1KB)
import { groupBy } from 'lodash-es';
const groups = groupBy(items, 'type');
// Even better in 2026: replace heavy libs entirely
import { format } from 'date-fns'; // tree-shakeable
// or the native Temporal API — zero bundle cost
const formatted = new Temporal.PlainDate.from(date);
Swap the heavyweights: moment (330KB) → date-fns or Temporal; full icon libraries → per-icon imports. Check every new dependency on Bundlephobia and record the compressed size and dependency count before approving it in review.
Code splitting: load what’s needed
Tree shaking removes dead code; code splitting defers live code to when it’s needed. Split by route and by heavy component using dynamic imports:
// Route-level splitting (React):
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Admin = React.lazy(() => import('./pages/Admin'));
// On-demand heavy feature:
const Chart = () => {
const [Lib, setLib] = useState(null);
useEffect(() => { import('recharts').then(setLib); }, []);
return Lib ? <Lib /> : <div className="skeleton" />;
};
Modern bundlers make splitting nearly automatic — Vite 6 handles route-based splitting out of the box. But there’s a contrarian caution: aggressive splitting increases download overhead and cache churn (many small files, more requests). Use chunk size limits and footprint budgets to govern fragmentation instead of splitting everything.
Auditing dependencies: the slow creep
Most bundle size problems aren’t one big mistake — they’re the accumulation of small, individually reasonable decisions. Every npm install is a decision to ship someone else’s code to every user’s browser. Build a repeatable audit loop:
- Before adding a library: check Bundlephobia for minified + gzipped size and dependency count.
- Verify tree-shakable entry — prefer ESM builds; many packages ship a CommonJS entry that blocks pruning.
- Watch transitive bloat — package A imports B imports C.
bundle-statsandstatoscopefind duplicate package instances and deep transitive inflation. - Audit in PRs — make bundle diffs part of code review so new bytes are visible before merge.
Enforce budgets in CI
Budgets are what stop the creep. Treat budget failures like test failures. Webpack has built-in performance hints; Lighthouse CI adds a browser-measured gate on every PR.
// webpack performance budget
{
"performance": {
"maxAssetSize": 250000,
"maxEntrypointSize": 400000,
"hints": "error"
}
}
# Lighthouse CI budget on every pull request
steps:
- uses: actions/checkout@v4
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
urls: ${{ steps.deploy.outputs.preview_url }}
budgetPath: ./budget.json
Make the failure actionable: size-limit --why and bundle-stats diffs tell the PR author exactly which module caused the delta.
The full optimization loop
Bundle size optimization follows a consistent pattern: measure → identify → replace or split → compress → enforce. The highest-impact moves:
- Replace
moment.jswithdate-fnsor the native Temporal API. - Add route-based code splitting.
- Enable Brotli/gzip compression on the server.
- Analyze with
webpack-bundle-analyzerand set CI budgets.
Teams routinely go from 2MB to 200KB with exactly these steps. Combine bundle analysis with field data (real user monitoring) to prioritize what actually impacts your users.
Putting It All Together
A complete workflow: run rollup-plugin-visualizer and find your five biggest modules. Replace the heavyweights with tree-shakeable or native alternatives. Add route-level and component-level code splitting. Set a CI budget (size-limit or Lighthouse CI) so the next PR that adds 100KB fails visibly. Re-run the analyzer — you’ll see the treemap shrink where it matters.
Conclusion & Next Steps
You now understand why JavaScript is the most expensive resource type, how tree shaking and code splitting work, how to audit dependencies, and how to enforce budgets in CI. Next steps: run the visualizer on your current app today, set your first CI budget, and adopt the habit of checking Bundlephobia before every new dependency.
References / Sources
- web.dev — JavaScript bundle performance guidance. https://web.dev
- webpack tree shaking guide. https://webpack.js.org/guides/tree-shaking
- Bundlephobia — bundle size of npm packages. https://bundlephobia.com
size-limit— cost measurement and CI budgets. https://github.com/ai/size-limit- webpack-bundle-analyzer. https://github.com/webpack/webpack-bundle-analyzer