Skip to main content

Bundle Optimization and Tree Shaking in JavaScript

Published: May 24, 2026 Updated: May 8, 2026 Larry Qu 7 min read

Bundle optimization is critical for performance. This article covers techniques for reducing bundle size through tree shaking, dead code elimination, and compression.

Introduction

Large bundles cause:

  • Slow initial load times
  • High bandwidth usage
  • Poor performance on slow networks
  • Wasted resources on unused code

Bundle optimization helps you:

  • Reduce bundle size by 30-70%
  • Improve Time to Interactive (TTI)
  • Reduce bandwidth costs
  • Improve user experience

Tree Shaking

What is Tree Shaking?

// math.js - Export multiple functions
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export function multiply(a, b) {
  return a * b;
}

export function divide(a, b) {
  return a / b;
}
// app.js - Only import what you need
import { add, multiply } from './math.js';

console.log(add(5, 3));
console.log(multiply(5, 3));

// Tree shaking removes subtract and divide from bundle

Enabling Tree Shaking

// webpack.config.js
module.exports = {
  mode: 'production', // Enables tree shaking
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  optimization: {
    usedExports: true, // Mark unused exports
    sideEffects: false // Assume no side effects
  }
};
// package.json
{
  "name": "my-package",
  "version": "1.0.0",
  "sideEffects": false, // Enable tree shaking for this package
  "main": "index.js",
  "module": "index.esm.js" // ES modules for tree shaking
}

Side Effects

// ❌ Bad: Side effects prevent tree shaking
// utils.js
console.log('Utils loaded'); // Side effect

export function helper() {
  return 'help';
}

// app.js
import { helper } from './utils.js'; // Entire module loaded due to side effect
// ✅ Good: No side effects
// utils.js
export function helper() {
  return 'help';
}

// app.js
import { helper } from './utils.js'; // Only helper is included
// Mark files with side effects
// webpack.config.js
module.exports = {
  optimization: {
    sideEffects: [
      '*.css', // CSS files have side effects
      './src/polyfills.js' // Polyfills have side effects
    ]
  }
};

Dead Code Elimination

Identifying Dead Code

// ❌ Bad: Unused code
function unusedFunction() {
  return 'never called';
}

function usedFunction() {
  return 'called';
}

console.log(usedFunction());

// unusedFunction is dead code
// ✅ Good: Remove unused code
function usedFunction() {
  return 'called';
}

console.log(usedFunction());

Conditional Code

// ❌ Bad: Conditional code not optimized
const isDevelopment = process.env.NODE_ENV === 'development';

if (isDevelopment) {
  console.log('Debug info');
  // Debug code included in production
}
// ✅ Good: Use DefinePlugin for dead code elimination
// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
};

// Now dead code is eliminated:
if (false) { // Eliminated by minifier
  console.log('Debug info');
}

Minification

JavaScript Minification

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

const result = calculateTotal([
  { price: 10 },
  { price: 20 }
]);

console.log(result);
// Minified code
function calculateTotal(e){let t=0;for(let l=0;l<e.length;l++)t+=e[l].price;return t}const result=calculateTotal([{price:10},{price:20}]);console.log(result);

// Size reduction: ~60%

Webpack Minification

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

module.exports = {
  mode: 'production', // Enables minification by default
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true // Remove console statements
          },
          mangle: true // Shorten variable names
        }
      })
    ]
  }
};

CSS Optimization

CSS Minification

// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  optimization: {
    minimizer: [
      new CssMinimizerPlugin()
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    })
  ]
};

Unused CSS Removal

// webpack.config.js
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const glob = require('glob');

module.exports = {
  plugins: [
    new PurgeCSSPlugin({
      paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, { nodir: true }),
      safelist: ['html', 'body'] // Keep these selectors
    })
  ]
};

Compression

Gzip Compression

// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 8192, // Only compress files > 8KB
      minRatio: 0.8 // Only compress if reduces size by 20%
    })
  ]
};

Brotli Compression

// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');
const zlib = require('zlib');

module.exports = {
  plugins: [
    new CompressionPlugin({
      algorithm: 'brotli',
      test: /\.(js|css|html|svg)$/,
      compressionOptions: {
        level: 11 // Maximum compression
      }
    })
  ]
};

Bundle Analysis

Webpack Bundle Analyzer

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

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

// Run: webpack --mode production
// View: bundle-report.html

Analyzing Bundle Composition

// Check what's in your bundle
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

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

// Parse bundle-stats.json to find large modules

Practical Optimization Strategies

1. Externalize Dependencies

// webpack.config.js
module.exports = {
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  }
};

// HTML:
// <script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>

2. Dynamic Imports

// ✅ Good: Load heavy libraries on-demand
async function loadChart() {
  const Chart = (await import('chart.js')).default;
  return Chart;
}

// Chart.js only loaded when needed

3. Polyfill Optimization

// ❌ Bad: Include all polyfills
import '@babel/polyfill';

// ✅ Good: Include only needed polyfills
import 'core-js/stable';
import 'regenerator-runtime/runtime';

4. Library Alternatives

// ❌ Bad: Large library
import moment from 'moment'; // 67KB

// ✅ Good: Smaller alternative
import { format } from 'date-fns'; // 13KB

Monitoring Bundle Size

CI/CD Integration

// scripts/check-bundle-size.js
const fs = require('fs');
const path = require('path');

function checkBundleSize() {
  const bundlePath = path.join(__dirname, '../dist/main.js');
  const stats = fs.statSync(bundlePath);
  const sizeInKB = stats.size / 1024;
  
  const maxSize = 250; // KB
  const maxGzipSize = 80; // KB
  
  console.log(`Bundle size: ${sizeInKB.toFixed(2)}KB`);
  
  if (sizeInKB > maxSize) {
    console.error(`❌ Bundle exceeds limit: ${sizeInKB.toFixed(2)}KB > ${maxSize}KB`);
    process.exit(1);
  }
  
  console.log(`✅ Bundle size OK`);
}

checkBundleSize();
// package.json
{
  "scripts": {
    "build": "webpack --mode production",
    "check-size": "npm run build && node scripts/check-bundle-size.js"
  }
}

Tracking Over Time

// scripts/track-bundle-size.js
const fs = require('fs');
const path = require('path');

function trackBundleSize() {
  const bundlePath = path.join(__dirname, '../dist/main.js');
  const stats = fs.statSync(bundlePath);
  const sizeInKB = stats.size / 1024;
  
  const historyFile = path.join(__dirname, '../bundle-history.json');
  let history = [];
  
  if (fs.existsSync(historyFile)) {
    history = JSON.parse(fs.readFileSync(historyFile, 'utf8'));
  }
  
  history.push({
    date: new Date().toISOString(),
    size: sizeInKB,
    commit: process.env.GIT_COMMIT || 'unknown'
  });
  
  fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
  
  console.log(`Bundle size tracked: ${sizeInKB.toFixed(2)}KB`);
}

trackBundleSize();

Best Practices

  1. Enable tree shaking:
    // ✅ Good
    module.exports = {
      mode: 'production',
      optimization: { usedExports: true }
    };
    ```javascript
    
  2. Use ES modules:
    // ✅ Good - enables tree shaking
    export function helper() {}
    
    // ❌ Bad - prevents tree shaking
    module.exports = { helper };
    ```javascript
    
  3. Avoid side effects:
    // ✅ Good - no side effects
    export function pure() {}
    
    // ❌ Bad - side effect
    console.log('loaded');
    export function impure() {}
    ```javascript
    
  4. Monitor bundle size:
    // ✅ Good - track in CI/CD
    npm run check-size
    ```javascript
    
  5. Use dynamic imports:
    // ✅ Good - load on-demand
    const module = await import('./heavy.js');
    ```javascript
    

Common Mistakes

  1. Not enabling tree shaking:
    // ❌ Bad
    module.exports = { mode: 'development' };
    
    // ✅ Good
    module.exports = { mode: 'production' };
    ```javascript
    
  2. Using CommonJS instead of ES modules:
    // ❌ Bad - prevents tree shaking
    module.exports = { helper };
    
    // ✅ Good - enables tree shaking
    export { helper };
    ```javascript
    
  3. Not analyzing bundle:
    // ❌ Bad - don't know what's in bundle
    // ✅ Good - use BundleAnalyzerPlugin
    ```javascript
    
  4. Including unnecessary dependencies:
    // ❌ Bad - large library
    import moment from 'moment';
    
    // ✅ Good - smaller alternative
    import { format } from 'date-fns';
    

Summary

Bundle optimization is essential for performance. Key takeaways:

  • Enable tree shaking to remove unused code
  • Use ES modules for better optimization
  • Minify JavaScript and CSS
  • Compress with gzip or brotli
  • Analyze bundle composition
  • Monitor bundle size in CI/CD
  • Use dynamic imports for large libraries
  • Choose smaller library alternatives

Next Steps

Resources

Comments

👍 Was this article helpful?