Skip to content
All essays
CraftMarch 27, 202415 min

Modern Bundle Optimization Techniques for Fast Web Apps

Master bundle optimization with tree shaking, code splitting, compression, and modern build tools for lightning-fast load times

Ü
Ümit Uz
Mobile & Full Stack Developer

Bundle optimization is crucial for delivering fast web applications. Large bundles lead to slow load times, poor user experience, and lower search rankings. Let's explore advanced techniques for optimizing your bundles.

Tree Shaking

Tree shaking eliminates dead code from your bundles. Modern bundlers use static analysis to remove unused exports.

ES6 Modules for Tree Shaking

javascript
// ❌ CommonJS - tree shaking doesn't work
const utils = require('./utils');
utils.util1(); // util2 and util3 still included

// ✅ ES6 modules - tree shaking works
import { util1 } from './utils';
util1(); // Only util1 included

// utils.js
export const util1 = () => { /* ... */ };
export const util2 = () => { /* ... */ }; // Removed if unused
export const util3 = () => { /* ... */ }; // Removed if unused

Side Effects Configuration

json
// package.json
{
  "sideEffects": false, // All modules are side-effect free
  "sideEffects": ["*.css", "*.scss"], // Only CSS has side effects
  "sideEffects": ["./src/polyfill.js"] // Specific files have side effects
}
javascript
// Mark pure functions for better tree shaking
export const pureFunction = /*#__PURE__*/ (() => {
  return function() {
    return 'pure';
  };
})();

Code Splitting

Route-Based Splitting

javascript
// React Router with lazy loading
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

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>
  );
}

Component-Based Splitting

javascript
// Lazy load heavy components
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>
        Show Chart
      </button>

      {showChart && (
        <Suspense fallback={<LoadingSpinner />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

Dynamic Imports

javascript
// Load modules on demand
async function processFile(file) {
  // Only load file processing library when needed
  const { parseFile, extractData } = await import('./lib/file-processor');
  return extractData(parseFile(file));
}

// Conditional loading
async function initializeMap() {
  if (needsMap) {
    const { initMap } = await import('./lib/map-loader');
    initMap();
  }
}

Webpack Optimization

Production Configuration

javascript
// webpack.prod.js
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  mode: 'production',

  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true, // Remove console.logs
            pure_funcs: ['console.log'], // Remove specific calls
          },
          format: {
            comments: false, // Remove comments
          },
        },
        extractComments: false, // Don't extract comments
      }),
      new CssMinimizerPlugin(),
    ],

    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },

    runtimeChunk: {
      name: 'runtime',
    },
  },

  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 8192, // Only compress files larger than 8KB
      minRatio: 0.8, // Only compress if compression ratio is better than 0.8
    }),
  ],
};

Module Resolution Optimization

javascript
module.exports = {
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'], // Avoid extensions in imports
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
    },
    modules: [path.resolve(__dirname, 'node_modules')], // Faster resolution
  },

  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true, // Cache babel results
            cacheCompression: false, // Faster cache
          },
        },
      },
    ],
  },
};

Vite Optimization

Build Configuration

javascript
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteCompression from 'vite-plugin-compression';

export default defineConfig({
  plugins: [
    react(),
    viteCompression({
      algorithm: 'gzip',
      ext: '.gz',
    }),
    viteCompression({
      algorithm: 'brotliCompress',
      ext: '.br',
    }),
  ],

  build: {
    target: 'esnext',
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },

    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          'ui-library': ['@mui/material', 'framer-motion'],
        },
      },
    },

    chunkSizeWarningLimit: 1000, // 1MB warning
  },

  esbuild: {
    drop: ['console', 'debugger'], // Remove console.logs
  },
});

Compression Strategies

Brotli Compression

javascript
// Express.js with Brotli compression
import express from 'express';
import compression from 'compression';
import expressStaticGzip from 'express-static-gzip';

const app = express();

// Serve pre-compressed files
app.use(
  expressStaticGzip('dist', {
    enableBrotli: true,
    orderPreference: ['br', 'gz'],
  })
);

// Fallback to on-the-fly compression
app.use(compression({
  threshold: 1024, // Only compress files larger than 1KB
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false;
    }
    return compression.filter(req, res);
  },
}));

Image Optimization

javascript
// Next.js Image Optimization
import Image from 'next/image';

export default function Page() {
  return (
    <Image
      src="/hero-image.jpg"
      alt="Hero"
      width={1200}
      height={600}
      priority // Above the fold
      placeholder="blur" // Blur placeholder
      blurDataURL="data:image/jpeg;base64,..."
    />
  );
}

// Modern image formats
// Automatically converts to WebP/AVIF

Runtime Optimization

Lazy Loading Images

javascript
// Intersection Observer for lazy loading
const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.srcset = img.dataset.srcset;
      img.classList.remove('lazy');
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img.lazy').forEach(img => {
  imageObserver.observe(img);
});

Prefetch and Preload

html
<!-- Preload critical resources -->
<link rel="preload" href="/styles/main.css" as="style">
<link rel="preload" href="/scripts/main.js" as="script">

<!-- Prefetch likely next pages -->
<link rel="prefetch" href="/dashboard">
<link rel="prefetch" href="/settings">

<!-- Preconnect to external origins -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">

Bundle Analysis

Webpack Bundle Analyzer

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

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

Performance Budgets

javascript
// webpack.config.js
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
  performance: {
    maxEntrypointSize: 244000, // 244KB
    maxAssetSize: 244000,
    hints: 'warning',
  },
});

Monitoring and Metrics

Bundle Size Tracking

javascript
// Track bundle sizes in CI
const fs = require('fs');
const gzipSize = require('gzip-size');

const files = [
  'dist/main.js',
  'dist/vendor.js',
  'dist/main.css',
];

files.forEach(file => {
  const content = fs.readFileSync(file);
  const size = content.length;
  const gzipped = gzipSize.sync(content);

  console.log(`${file}:`);
  console.log(`  Size: ${(size / 1024).toFixed(2)} KB`);
  console.log(`  Gzipped: ${(gzipped / 1024).toFixed(2)} KB`);

  if (gzipped > 244000) {
    console.error(`  ⚠️  Exceeds 244KB budget!`);
    process.exit(1);
  }
});

Best Practices

  1. 1Measure First: Always analyze bundle size before optimizing
  2. 2Lazy Load Routes: Split by route for optimal initial load
  3. 3Tree Shake Aggressively: Use ES6 modules and mark side-effect-free code
  4. 4Compress Everything: Use Brotli and Gzip for text-based assets
  5. 5Optimize Images: Use modern formats and responsive images
  6. 6Set Budgets: Enforce bundle size limits in CI/CD
  7. 7Monitor Continuously: Track bundle sizes over time
  8. 8Update Dependencies: Newer versions often include optimizations

Conclusion

Bundle optimization is an ongoing process. Start with code splitting and tree shaking, implement compression strategies, and continuously monitor your bundle sizes. The key is to deliver only what's needed, when it's needed, in the most efficient format possible.

Related essays

Next essay
Advanced Code Splitting Strategies for Modern Web Apps