Skip to main content
โšก Calmops

Bundle Optimization and Tree Shaking in JavaScript

Bundle Optimization and Tree Shaking in JavaScript

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 }
    };
    
  2. Use ES modules:

    // โœ… Good - enables tree shaking
    export function helper() {}
    
    // โŒ Bad - prevents tree shaking
    module.exports = { helper };
    
  3. Avoid side effects:

    // โœ… Good - no side effects
    export function pure() {}
    
    // โŒ Bad - side effect
    console.log('loaded');
    export function impure() {}
    
  4. Monitor bundle size:

    // โœ… Good - track in CI/CD
    npm run check-size
    
  5. Use dynamic imports:

    // โœ… Good - load on-demand
    const module = await import('./heavy.js');
    

Common Mistakes

  1. Not enabling tree shaking:

    // โŒ Bad
    module.exports = { mode: 'development' };
    
    // โœ… Good
    module.exports = { mode: 'production' };
    
  2. Using CommonJS instead of ES modules:

    // โŒ Bad - prevents tree shaking
    module.exports = { helper };
    
    // โœ… Good - enables tree shaking
    export { helper };
    
  3. Not analyzing bundle:

    // โŒ Bad - don't know what's in bundle
    // โœ… Good - use BundleAnalyzerPlugin
    
  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

Comments