JavaScript Bundle Size Optimization: From Analysis to Action

May 28, 20267 min read

JavaScript bundle size directly impacts page load performance. Every kilobyte of JavaScript must be downloaded, parsed, compiled, and executed — and the cost compounds on mobile devices with slower CPUs and networks. This guide covers practical strategies to shrink your bundles, from quick wins to architectural changes.

Why Bundle Size Matters

JavaScript is the most expensive resource on the web. Unlike images, which decode progressively, JavaScript blocks the main thread during parsing and compilation.

Resource TypeProcessing CostTypical Size
HTMLParse + render10–50 KB
CSSParse + style calculation20–100 KB
JavaScriptParse + compile + execute100–500 KB+
ImagesDecode + paint50–500 KB+

A 200 KB JavaScript bundle takes roughly 5–10 seconds to become interactive on a mid-range mobile device. The same 200 KB as a JPEG appears in under a second. Reducing bundle size is not optional — it is a performance requirement.

Analyze Your Bundle First

Before optimizing, measure. You cannot shrink what you cannot see.

Webpack Bundle Analyzer:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  plugins: [new BundleAnalyzerPlugin()]
}

Vite (Rollup Visualizer):

npm install rollup-plugin-visualizer --save-dev
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [visualizer({ open: true })]
})

Source Map Explorer (works with any bundler):

npx source-map-explorer dist/assets/*.js

Look for: large dependencies you did not expect, duplicate versions of the same package, and modules you import but barely use.

Tree Shaking and Dead Code Elimination

Tree shaking removes exported code that no other module imports. It relies on ES module static imports (import { foo } from 'bar') — CommonJS require() calls cannot be statically analyzed.

Enable tree shaking:

  • Use ES module syntax consistently
  • Mark packages with "sideEffects": false in package.json (only if true)
  • Avoid barrel files that re-export everything from a module
// BAD: imports entire lodash (72 KB gzipped)
import _ from 'lodash'
const result = _.debounce(fn, 300)

// GOOD: imports only debounce (~1 KB gzipped)
import debounce from 'lodash/debounce'
const result = debounce(fn, 300)

// BEST: use lodash-es for tree shaking
import { debounce } from 'lodash-es'

Barrel files (index.js that re-exports everything) are a common tree-shaking killer:

// apis/index.js — forces bundler to include ALL APIs
export { Users } from './users'
export { Products } from './products'
export { Orders } from './orders'
export { Analytics } from './analytics'  // rarely used but always bundled

Import directly from the sub-module instead: import { Users } from './apis/users'.

Code Splitting and Dynamic Imports

Code splitting breaks your bundle into smaller chunks loaded on demand. The simplest method is dynamic import():

// Loaded immediately — part of the main bundle
import { navigationInit } from './navigation'

// Loaded only when the user opens settings
async function openSettings() {
  const { SettingsPanel } = await import('./settings-panel.vue')
  render(SettingsPanel)
}

Route-level splitting is the most effective pattern:

// Nuxt — automatic route splitting
// Each page in app/pages/ becomes a separate chunk

// Vue Router — manual route splitting
const routes = [
  {
    path: '/dashboard',
    component: () => import('./pages/Dashboard.vue')
  },
  {
    path: '/settings',
    component: () => import('./pages/Settings.vue')
  }
]

This ensures users only download code for the page they are viewing — not the entire application upfront.

Dependency Auditing

Large dependencies are the biggest source of bundle bloat. Audit regularly:

# Find the largest packages in your bundle
npx bundlephobia <package-name>

# Check for duplicate dependencies
npx npm-check

# Analyze full bundle cost
npx cost-of-modules

Common heavy packages and lighter alternatives:

Heavy PackageSize (gzip)Lighter AlternativeSize (gzip)
moment67 KBdate-fns (per-function)2–5 KB
lodash72 KBlodash-es (tree-shakeable)1–5 KB
rxjs32 KBnanothrow or native events<1 KB
axios13 KBfetch (native)0 KB

Before adding a dependency, check its cost onBundlephobia. A 5 KB package that replaces ten lines of code is a bad trade.

Compression and Caching

Smaller bundles compress better. Enable Brotli or gzip at your hosting layer:

CompressionTypical JS Reduction
gzip (level 6)65–75%
Brotli (level 4)70–80%

Pre-compress static assets at build time to avoid runtime CPU cost. Vite and Webpack both support plugins for this.

Cache aggressively with content-based filenames:

<script src="/assets/app.a3f7b2c4.js"></script>

When the content changes, the hash changes, and browsers fetch the new version. Until then, the cached copy serves instantly.

Key Takeaways

  • JavaScript is the most expensive web resource — every KB counts for parsing and execution
  • Analyze your bundle before optimizing; visualizers reveal unexpected bloat
  • Use ES module imports and avoid barrel files to enable effective tree shaking
  • Split code at route boundaries with dynamic import() — users download only what they need
  • Audit dependencies regularly — replacing moment with date-fns can save 60+ KB
  • Enable Brotli compression and content-hash caching for production

Try It Yourself

Minify your JavaScript and see the size difference with our free JavaScript Minifier. Paste your code, compare original vs. minified output, and understand how much you can save before even applying compression.