返回博客

JavaScript Bundle Size Optimization: Reduce Your App's Footprint

Complete guide to reducing JavaScript bundle size. Learn tree-shaking, code-splitting, dynamic imports, and compression techniques to build faster web applications with smaller payloads.

13 min read
作者:Perf Lens Team

Introduction

JavaScript bundle size directly impacts your application's load time, and load time directly affects user experience and SEO rankings. According to HTTP Archive, the median JavaScript bundle size for websites is ~460 KB, but the top 10% of sites serve less than 100 KB.

Large bundles mean:

  • ❌ Slower page loads (especially on mobile)
  • ❌ Higher bandwidth costs
  • ❌ Poor user experience
  • ❌ Lower SEO rankings (Core Web Vitals)
  • ❌ Reduced conversion rates

In this comprehensive guide, we'll:

  • Analyze what makes bundles large
  • Learn proven optimization techniques
  • Implement tree-shaking and code-splitting
  • Measure bundle size improvements
  • Test everything using Perf Lens for accurate measurements

By the end of this article, you'll know how to build lean, fast-loading web applications.


Understanding Bundle Size

What's in Your Bundle?

A typical JavaScript bundle includes:

  • 1. Your application code** (30-40%)
  • 2. Third-party libraries** (40-50%)
  • 3. Framework code** (React, Vue, Angular) (10-20%)
  • 4. Polyfills and utilities** (5-10%)

Measuring Bundle Size

Development vs Production:

# Development bundle (unminified)
main.js: 2.5 MB

# Production bundle (minified)
main.js: 450 KB (minified)
main.js: 120 KB (minified + gzipped)
main.js: 95 KB (minified + brotli)

Key Metrics:

  • Raw size: Actual file size on disk
  • Minified size: After removing whitespace and comments
  • Gzipped size: After compression (70-80% smaller)
  • Brotli size: Better compression (~20% smaller than gzip)

Optimization Technique #1: Tree-Shaking

What is Tree-Shaking?

Tree-shaking eliminates dead code by only including functions you actually use.

❌ Without Tree-Shaking:

// Imports entire lodash library (70 KB)
import _ from 'lodash';

const result = _.debounce(myFunction, 300);

✅ With Tree-Shaking:

// Imports only debounce function (2 KB)
import debounce from 'lodash/debounce';

const result = debounce(myFunction, 300);

Savings: 68 KB (97% reduction!)


How to Enable Tree-Shaking

1. Use ES6 Module Syntax:

// ✅ Good (tree-shakeable)
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }

// ❌ Bad (not tree-shakeable)
module.exports = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};

2. Configure Webpack:

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    sideEffects: false
  }
};

3. Mark Side Effects in package.json:

{
  "name": "my-library",
  "sideEffects": false
}

Or specify files with side effects:

{
  "sideEffects": ["*.css", "*.scss"]
}

Tree-Shaking Best Practices

Use Named Imports:

// ✅ Good (tree-shakeable)
import { Button, Input } from 'my-ui-library';

// ❌ Bad (imports everything)
import * as UI from 'my-ui-library';

Check Library Support:

// Some libraries don't support tree-shaking
// Check before installing:

// ✅ Good (tree-shakeable)
import { format } from 'date-fns';

// ❌ Bad (not tree-shakeable in older versions)
import moment from 'moment';

Optimization Technique #2: Code-Splitting

What is Code-Splitting?

Code-splitting breaks your bundle into smaller chunks that load on-demand.

Before Code-Splitting:

main.js (500 KB) ──┐
                   ├─> User downloads everything
                   └─> Even unused routes!

After Code-Splitting:

main.js (100 KB) ──> Initial load
routes/about.js (50 KB) ──> Loads when visited
routes/admin.js (80 KB) ──> Loads when visited

Dynamic Imports

React Example:

// ❌ Static import (included in main bundle)
import HeavyComponent from './HeavyComponent';

function App() {
  return <HeavyComponent />;
}
// ✅ Dynamic import (loaded on demand)
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

Route-Based Splitting

React Router Example:

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// Lazy-load route components
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Bundle Analysis:

Before:
  main.js: 500 KB

After:
  main.js: 120 KB (Home page)
  about.chunk.js: 80 KB (loads when /about visited)
  dashboard.chunk.js: 180 KB (loads when /dashboard visited)

Improvement: Initial load reduced by 76%!


Component-Level Splitting

Modal Example:

// Modal only used when user clicks button
const Modal = lazy(() => import('./Modal'));

function App() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <button onClick={() => setShowModal(true)}>
        Open Modal
      </button>
      {showModal && (
        <Suspense fallback={null}>
          <Modal onClose={() => setShowModal(false)} />
        </Suspense>
      )}
    </div>
  );
}

Optimization Technique #3: Library Alternatives

Replace Heavy Libraries

Many popular libraries have lightweight alternatives.

Moment.js → date-fns or Day.js:

// ❌ Moment.js (232 KB minified)
import moment from 'moment';
const date = moment().format('YYYY-MM-DD');

// ✅ date-fns (13 KB minified, tree-shakeable!)
import { format } from 'date-fns';
const date = format(new Date(), 'yyyy-MM-dd');

// ✅ Day.js (7 KB minified)
import dayjs from 'dayjs';
const date = dayjs().format('YYYY-MM-DD');

Savings: 225 KB (97% reduction!)


Lodash → Native Methods + Micro Libraries:

// ❌ Lodash (71 KB minified)
import _ from 'lodash';
const unique = _.uniq(array);
const sorted = _.sortBy(array, 'name');

// ✅ Native + tree-shakeable imports
import uniq from 'lodash/uniq';
const unique = [...new Set(array)]; // Native
const sorted = array.sort((a, b) => a.name.localeCompare(b.name));

Axios → Fetch API + ky:

// ❌ Axios (13 KB minified)
import axios from 'axios';
const { data } = await axios.get('/api/users');

// ✅ Fetch API (native, 0 KB!)
const response = await fetch('/api/users');
const data = await response.json();

// ✅ ky (lightweight wrapper, 4 KB)
import ky from 'ky';
const data = await ky.get('/api/users').json();

Library Size Comparison Table

LibrarySize (min)AlternativeSize (min)Savings
Moment.js232 KBdate-fns13 KB95%
Lodash (full)71 KBLodash/fp25 KB65%
jQuery87 KBVanilla JS0 KB100%
Axios13 KBFetch API0 KB100%
Chart.js236 KBRecharts95 KB60%

Optimization Technique #4: Minification & Compression

Minification

Minification removes unnecessary characters without changing functionality.

Before:

function calculateTotal(items) {
  let total = 0;
  for (let i = 0; i < items.length; i++) {
    total += items[i].price;
  }
  return total;
}

After (Terser):

function calculateTotal(t){let e=0;for(let l=0;l<t.length;l++)e+=t[l].price;return e}

Webpack Configuration:

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true, // Remove console.log
            drop_debugger: true
          }
        }
      })
    ]
  }
};

Compression

Gzip vs Brotli:

# Original
main.js: 450 KB

# Gzipped (default)
main.js.gz: 120 KB (73% reduction)

# Brotli (better)
main.js.br: 95 KB (79% reduction)

Enable Brotli in Next.js:

// next.config.js
module.exports = {
  compress: true, // Enables gzip by default

  // For Brotli, configure in your hosting (Vercel, Netlify, etc.)
  // or use a custom server
};

Nginx Configuration:

# Enable Brotli
brotli on;
brotli_types text/plain text/css application/json application/javascript text/xml;
brotli_comp_level 6;

Optimization Technique #5: Analyzing Your Bundle

Webpack Bundle Analyzer

npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html'
    })
  ]
};

Run Analysis:

npm run build
# Opens interactive treemap visualization

Next.js Bundle Analyzer

npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
});

module.exports = withBundleAnalyzer({
  // your next.js config
});
ANALYZE=true npm run build

What to Look For

Red Flags:

  • 🚩 Duplicate dependencies (multiple versions)
  • 🚩 Unused libraries (imported but not used)
  • 🚩 Heavy libraries (>50 KB for single feature)
  • 🚩 Entire library imported when only one function needed

Example Findings:

❌ lodash (71 KB) - only using 2 functions
❌ moment (232 KB) - only for date formatting
❌ react-icons (2.5 MB) - importing all icons
✅ date-fns (13 KB) - tree-shakeable

Real-World Optimization Example

Before Optimization

Bundle Analysis:

main.js: 850 KB (minified)
├── react + react-dom: 140 KB
├── moment.js: 232 KB
├── lodash: 71 KB
├── chart.js: 236 KB
├── react-icons (all): 2.5 MB (tree-shaken to 120 KB)
└── application code: 51 KB

Performance:

  • Load Time: 4.2s (3G)
  • First Contentful Paint: 2.8s
  • Time to Interactive: 5.1s

After Optimization

Changes Made:

  1. ✅ Replaced Moment.js → date-fns (13 KB)
  2. ✅ Replaced full Lodash → specific imports (8 KB)
  3. ✅ Replaced Chart.js → Recharts (95 KB)
  4. ✅ Implemented route-based code splitting
  5. ✅ Enabled Brotli compression

New Bundle Analysis:

main.js: 180 KB (minified + brotli)
├── react + react-dom: 140 KB
├── date-fns: 13 KB
├── lodash (tree-shaken): 8 KB
├── application code: 51 KB
└── Code-split routes: 150 KB (loaded on demand)

New Performance:

  • Load Time: 1.1s (3G) ✅ 74% faster
  • First Contentful Paint: 0.8s ✅ 71% faster
  • Time to Interactive: 1.5s ✅ 71% faster

Advanced Optimization Techniques

1. Preloading Critical Assets

<!-- Preload critical JavaScript -->
<link rel="preload" href="/main.js" as="script">

<!-- Preload critical CSS -->
<link rel="preload" href="/styles.css" as="style">

<!-- Preload fonts -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

2. Defer Non-Critical JavaScript

<!-- Defer analytics until after page load -->
<script defer src="/analytics.js"></script>

<!-- Async for third-party scripts -->
<script async src="https://example.com/widget.js"></script>

3. Inline Critical CSS

<!-- Inline critical CSS (above-the-fold styles) -->
<style>
  .header { /* critical styles */ }
  .hero { /* critical styles */ }
</style>

<!-- Load rest asynchronously -->
<link rel="stylesheet" href="/main.css" media="print" onload="this.media='all'">

4. Remove Unused CSS

PurgeCSS Configuration:

// postcss.config.js
module.exports = {
  plugins: [
    require('@fullhuman/postcss-purgecss')({
      content: ['./src/**/*.{js,jsx,ts,tsx}'],
      defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
    })
  ]
};

Results:

Before: styles.css (450 KB)
After: styles.css (45 KB) ✅ 90% reduction

5. Image Optimization

Images often larger than JavaScript!

// Next.js Image component
import Image from 'next/image';

<Image
  src="/photo.jpg"
  width={800}
  height={600}
  alt="Description"
  loading="lazy"
  quality={75} // Reduce quality for smaller file size
/>

Modern Formats:

  • WebP (30% smaller than JPEG)
  • AVIF (50% smaller than JPEG)

Performance Budget

Set limits to prevent bundle bloat.

Example Budget:

{
  "budgets": [
    {
      "path": "main.js",
      "maxSize": "150 KB",
      "maxSizeGzip": "50 KB"
    },
    {
      "path": "*.css",
      "maxSize": "50 KB"
    }
  ]
}

Webpack Performance Hints:

// webpack.config.js
module.exports = {
  performance: {
    maxAssetSize: 150000, // 150 KB
    maxEntrypointSize: 150000,
    hints: 'error' // Fail build if exceeded
  }
};

Monitoring Bundle Size

CI/CD Integration

# GitHub Actions
name: Bundle Size Check

on: [pull_request]

jobs:
  check-size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: npm ci
      - run: npm run build
      - uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

Size Limit Tool

npm install --save-dev size-limit @size-limit/file
// package.json
{
  "size-limit": [
    {
      "path": "dist/main.js",
      "limit": "150 KB"
    }
  ]
}
npm run size-limit
# ✓ dist/main.js: 145 KB (within limit)

Testing Bundle Size

Use Perf Lens to measure the impact of your code.

Test Case: Library Comparison

// Test 1: Full Lodash
import _ from 'lodash';
const result = _.uniq([1, 2, 2, 3]);

// Test 2: Tree-shaken Lodash
import uniq from 'lodash/uniq';
const result = uniq([1, 2, 2, 3]);

// Test 3: Native
const result = [...new Set([1, 2, 2, 3])];

// Compare bundle sizes and performance

Best Practices Checklist

✅ Essential Optimizations

  • Enable tree-shaking (ES6 modules)
  • Implement code-splitting (routes + components)
  • Use dynamic imports for heavy components
  • Enable minification (Terser/UglifyJS)
  • Enable Brotli compression
  • Remove console.log in production
  • Analyze bundle with webpack-bundle-analyzer

✅ Library Management

  • Review all dependencies regularly
  • Replace heavy libraries with alternatives
  • Use tree-shakeable libraries
  • Import only what you need
  • Remove unused dependencies

✅ Monitoring

  • Set up performance budget
  • Monitor bundle size in CI/CD
  • Track Core Web Vitals
  • Regular bundle audits (monthly)

Common Mistakes to Avoid

❌ Mistake 1: Importing Entire Libraries

// ❌ Bad (imports 71 KB)
import _ from 'lodash';
const result = _.debounce(fn, 300);

// ✅ Good (imports 2 KB)
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);

❌ Mistake 2: Not Using Code-Splitting

// ❌ Bad (500 KB initial load)
import AdminPanel from './AdminPanel'; // 200 KB
import ReportGenerator from './ReportGenerator'; // 150 KB

// ✅ Good (100 KB initial, load on demand)
const AdminPanel = lazy(() => import('./AdminPanel'));
const ReportGenerator = lazy(() => import('./ReportGenerator'));

❌ Mistake 3: Including Dev Dependencies in Production

// ❌ Bad
{
  "dependencies": {
    "react": "^18.0.0",
    "webpack": "^5.0.0",  // Should be devDependency!
    "@testing-library/react": "^13.0.0"  // Should be devDependency!
  }
}

// ✅ Good
{
  "dependencies": {
    "react": "^18.0.0"
  },
  "devDependencies": {
    "webpack": "^5.0.0",
    "@testing-library/react": "^13.0.0"
  }
}

Conclusion

Optimizing JavaScript bundle size is crucial for building fast web applications. By implementing these techniques, you can achieve:

Key Takeaways:

  • 1. Tree-shaking** eliminates unused code (50-90% reduction)
  • 2. Code-splitting** loads code on-demand (70-80% initial load reduction)
  • 3. Library alternatives** can save hundreds of KB
  • 4. Compression** (Brotli) reduces transfer size by 70-80%
  • 5. Monitoring** prevents regression

Optimization Impact:

  • Bundle Size: 850 KB → 180 KB (79% reduction)
  • Load Time: 4.2s → 1.1s (74% faster)
  • User Experience: Dramatically improved
  • SEO: Better Core Web Vitals scores

Action Plan:

  • 1. Analyze** your current bundle (webpack-bundle-analyzer)
  • 2. Replace** heavy libraries with alternatives
  • 3. Implement** code-splitting for routes
  • 4. Enable** tree-shaking and compression
  • 5. Monitor** bundle size in CI/CD

Remember: Every KB counts! A 100 KB reduction can mean the difference between a user staying or bouncing.


Further Reading


Want to measure your bundle's performance impact? Test your code with Perf Lens to see real-world execution time and memory usage differences!

JavaScript Bundle Size Optimization: Reduce Your App's Footprint | Perf Lens