All posts
JavaScriptPerformanceTBT

How to Reduce JavaScript Bundle Size

Large JavaScript bundles are the #1 cause of slow TBT and high INP. Learn how to audit your bundle, remove unused code, and split it for faster load times.

debugutils.dev

JavaScript is expensive in a way that other assets aren’t. A 500KB image costs bandwidth but only occupies one thread. A 500KB JavaScript file costs bandwidth and must be parsed and executed on the main thread — potentially blocking it for seconds on a mid-range mobile device.

Large JavaScript bundles are the primary driver of high Total Blocking Time (TBT) and poor INP scores. Reducing bundle size is the most effective way to improve responsiveness.

Audit Your Current Bundle

Before optimizing, understand what’s in your bundle.

For webpack:

npm install --save-dev webpack-bundle-analyzer

Add to webpack.config.js:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = { plugins: [new BundleAnalyzerPlugin()] };

For Vite:

npm install --save-dev rollup-plugin-visualizer

Bundlephobia — paste any npm package name to see its minified+gzipped size and tree-shakability.

Look for:

  • Packages that are much larger than expected
  • Duplicate dependencies (multiple versions of lodash, etc.)
  • Packages where you’re only using a small portion of their functionality
  • Polyfills that aren’t needed for your browser targets

Remove Unused Code (Tree Shaking)

Modern bundlers (webpack, Rollup, esbuild, Vite) support tree shaking — they remove exports that aren’t imported anywhere. But it only works with ES modules (import/export), not CommonJS (require).

Make sure you’re using named imports rather than whole packages:

// Bad — imports the entire lodash library (~70KB)
import _ from 'lodash';
const sorted = _.sortBy(data, 'name');

// Good — imports only what you need (~1KB)
import sortBy from 'lodash/sortBy';
const sorted = sortBy(data, 'name');

For date libraries, consider date-fns over moment.js — it’s tree-shakable and much lighter.

Replace Heavy Dependencies

Some popular packages have lighter alternatives with similar APIs:

Heavy (minified, uncompressed)Lightweight alternative
moment.js (72KB)date-fns or dayjs (2–5KB)
lodash (full, 70KB)Native JS methods or lodash-es
axios (13KB)fetch API (built-in)
jQuery (87KB)Vanilla JS (usually)
chart.js (187KB)lightweight-charts or D3 subset

Always check if the browser API now covers what the library does. Most modern JS apps don’t need jQuery or Lodash at all.

Code Split by Route

If you’re using a framework like React, Vue, or Svelte, don’t load all your code on every page. Split your bundle by route so users only download what they need.

React with React Router:

import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Route path="/dashboard" element={<Dashboard />} />
    </Suspense>
  );
}

Vite handles code splitting automatically when you use dynamic import().

Use Dynamic Imports for Non-Critical Features

Components and features that aren’t needed on initial load should be loaded on demand:

// Load a heavy charting library only when the user navigates to the analytics tab
const loadChart = async () => {
  const { Chart } = await import('chart.js');
  return new Chart(ctx, config);
};

tabButton.addEventListener('click', loadChart);

Set Correct Browser Targets

If you’re transpiling ES2015+ to ES5 for older browsers, you may be generating significantly more code. Update your Babel or TypeScript target to the browsers you actually support:

last 2 Chrome versions
last 2 Firefox versions
last 2 Safari versions

This can reduce bundle size by 10–20% for codebases that use modern JS features heavily.

Enable Compression

Make sure your server is serving JavaScript with gzip or Brotli compression. Brotli typically achieves 15–25% better compression than gzip.

Most CDNs and hosting platforms do this automatically, but verify it in your request waterfall — look for content-encoding: br or content-encoding: gzip headers.

Measuring the Impact

Run a before/after test on debugutils.dev to see the impact of your changes on TBT, LCP, and overall performance score. The waterfall view shows each JavaScript file’s transfer size, so you can confirm compression and see download times.