/ JS Checker Fixes / JS Bundle Size

How to Fix JS Bundle Size

Heavy JS bundles cost more than just bandwidth — they cost parse time, compile time, and Time To Interactive on every page load. Mobile devices feel the parsing cost most: a 500 KB bundle can take 2-4 seconds to parse on a mid-range phone. The cure is auditing what's in the bundle, tree-shaking, replacing heavy libraries with lighter alternatives, and code-splitting per route. Typical sites cut bundle size 50-70% applying these techniques. For related fixes, see the JS Checker Fixes index.

1. Audit what's in your bundle

Step 1
Install a bundle analyser
# webpack
npm install --save-dev webpack-bundle-analyzer

# Rollup / Vite
npm install --save-dev rollup-plugin-visualizer

# Next.js
ANALYZE=true npm run build
Step 2
Read the visualisation
Bundle analysers produce treemap views: each block is a module, sized proportionally to its bytes in the bundle. Largest blocks are your biggest opportunities. Common offenders: lodash, moment, jQuery duplicated by dependencies, large UI libraries.

2. Tree-shake unused exports

Tree-shaking requires ES modules and named imports. CommonJS imports keep everything.

Lodash (worst offender)

// Bad: imports all of lodash (24 KB+ per chunk)
import _ from 'lodash';
_.debounce(...);

// Better: named import (still imports lodash internally, doesn't tree-shake)
import { debounce } from 'lodash';

// Best: lodash-es supports tree-shaking
import { debounce } from 'lodash-es';

// Or: per-function import
import debounce from 'lodash/debounce';

date-fns over moment

// Bad: moment.js — 232 KB minified+gzipped with all locales
import moment from 'moment';
moment().format('YYYY-MM-DD');

// Good: date-fns — only the functions you use, ~5-15 KB total
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');

Configure sideEffects

// package.json
{
  "sideEffects": false,  // your code has no side-effect imports
  // OR list specific files that DO have side effects
  "sideEffects": ["*.css", "./src/polyfills.js"]
}

3. Replace heavy libraries

HeavyLighter alternative
moment (~232 KB)date-fns (~15 KB), dayjs (~7 KB), Temporal (native)
lodash (~24 KB)lodash-es with tree shaking, native methods
axios (~13 KB)native fetch (0 KB), ky (~5 KB)
jQuery (~30 KB)native DOM APIs, querySelector, fetch
Chart.js (~60 KB)Chartist (~15 KB), uPlot (~40 KB faster)
Animation libraries (~30-100 KB)CSS transitions, Web Animations API (native)

Check bundlephobia.com for size of any npm package and tree-shakeability.

4. Code-split per route

// Static import: everything in initial bundle
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
import Reports from './pages/Reports';

// Dynamic import: each becomes its own chunk
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Reports = lazy(() => import('./pages/Reports'));

Build outputs separate JS files per route. User loading homepage doesn't download Settings or Reports code. Each navigation triggers loading only that route's chunk.

Common splitting points

5. Remove dead dependencies

Step 1
Find unused npm packages
npx depcheck

# Shows unused dependencies, missing dependencies, unused devDependencies
Manually verify before removing — depcheck can miss dynamic imports.
Step 2
Audit dependency tree depth
npm ls --depth=2
Look for: same library at multiple versions (caused by transitive dependencies), unexpected huge packages installed indirectly.

6. Compress and cache

Brotli over gzip

# nginx
brotli on;
brotli_types text/css application/javascript application/json text/html;
brotli_comp_level 6;

# Pre-compress at build time for static files
npm install --save-dev compression-webpack-plugin
new CompressionPlugin({
  algorithm: 'brotliCompress',
  test: /\.(js|css)$/
});

Immutable cache headers on hashed filenames

location ~* \.(js|css)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

Production build outputs main.abc123.js. Hash changes when content changes. Cache for a year because the file URL changes on update. Repeat visits pay zero JS download.

7. Set a budget in CI

# Lighthouse CI
{
  "ci": {
    "assert": {
      "assertions": {
        "resource-summary:script:size": ["error", { "maxNumericValue": 200000 }]
      }
    }
  }
}

Fails build if JS exceeds 200 KB. Keeps regressions out.

💡 Bundle size optimisation has diminishing returns. Cut the big stuff first (heavy libraries, untree-shaken imports, missing splitting), then stop. Spending hours saving 2 KB is rarely the best use of time when there are other performance fixes to ship.

⚙️ Re-run the JS Checker

Verify bundle size has dropped to budget.

Run JS Checker →
Related Guides: JS Checker Fixes  ·  Fix Render-Blocking JS  ·  Fix Long Tasks  ·  JS Checker Guide
💬 Got a problem?