Module Bundlers

Module bundlers combine multiple JavaScript files into optimized bundles for browsers. They handle dependencies, code transformation, and code splitting for performance. They enable the use of modern JavaScript features, improve loading times, and streamline development workflows.

Examples: Webpack, Rollup, Parcel, Vite, esbuild, SWC...

Why Use a Module Bundler?

Problems Solved

  • Dependency Management - Automatic resolution of imports/exports
  • Browser Compatibility - Transformation of ES6+ to ES5
  • Optimization - Minification, tree-shaking, code splitting
  • Modern Development - Hot reload, source maps, TypeScript
  • Performance - Intelligent bundling, lazy loading

Without a Bundler (Problems)

<!-- Manual dependency management -->
<script src="lib/jquery.js"></script>
<script src="lib/lodash.js"></script>
<script src="utils/helpers.js"></script>
<script src="components/header.js"></script>
<script src="components/footer.js"></script>
<script src="app.js"></script>
<!-- Important order, no modules, global pollution -->

With a Bundler

// Modern imports
import { debounce } from 'lodash'
import { fetchUser } from './api/users'
import Header from './components/Header'

// Modern code, automatic dependency management

The Main Bundlers

Advantages:

  • Ultra-fast startup (native ESM in dev)
  • Instant hot reload
  • Minimal configuration
  • Native TypeScript/JSX support
  • Rollup in production

Disadvantages:

  • Newer (fewer resources)
  • Fewer plugins than Webpack
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash', 'date-fns']
        }
      }
    }
  },
  server: {
    port: 3000,
    open: true
  }
})

Advantages:

  • Huge ecosystem
  • Very flexible configuration
  • Loaders for everything (CSS, images, fonts...)
  • Advanced code splitting
  • Hot Module Replacement

Disadvantages:

  • Complex configuration
  • Slow build times on large projects
  • Steep learning curve
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  entry: './src/index.js',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    clean: true
  },
  
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react']
          }
        }
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        type: 'asset/resource'
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    })
  ],
  
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  },
  
  devServer: {
    port: 3000,
    hot: true,
    open: true
  }
}

Rollup (Ideal for libraries)

Advantages:

  • Highly optimized bundles
  • Excellent tree-shaking
  • Simple configuration
  • Perfect for libraries
  • Native ES modules

Disadvantages:

  • Fewer features for apps
  • Smaller ecosystem
  • Limited code splitting
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'

export default {
  input: 'src/index.js',
  
  output: [
    {
      file: 'dist/bundle.cjs.js',
      format: 'cjs',
      sourcemap: true
    },
    {
      file: 'dist/bundle.esm.js',
      format: 'esm',
      sourcemap: true
    },
    {
      file: 'dist/bundle.umd.js',
      format: 'umd',
      name: 'MyLibrary',
      sourcemap: true
    }
  ],
  
  plugins: [
    resolve({
      browser: true,
      preferBuiltins: false
    }),
    commonjs(),
    babel({
      babelHelpers: 'bundled',
      exclude: 'node_modules/**',
      presets: ['@babel/preset-env']
    }),
    terser() // Minification
  ],
  
  external: ['react', 'react-dom'] // External dependencies
}

Parcel (Zero-config)

Advantages:

  • Zero configuration
  • Very fast
  • Native multi-format support
  • Excellent hot reload
  • Intelligent cache

Disadvantages:

  • Less control
  • Smaller ecosystem
  • Limited customization
// package.json (enough for Parcel!)
{
  "scripts": {
    "dev": "parcel src/index.html",
    "build": "parcel build src/index.html --dist-dir dist"
  }
}

esbuild (Ultra-fast)

Advantages:

  • Extreme speed (written in Go)
  • Native TypeScript/JSX support
  • Simple API
  • Very fast minification

Disadvantages:

  • Limited features
  • No native Hot reload
  • Nascent ecosystem
// build.js
const esbuild = require('esbuild')

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  minify: true,
  sourcemap: true,
  target: 'es2015',
  loader: {
    '.png': 'file',
    '.svg': 'text'
  },
  define: {
    'process.env.NODE_ENV': '"production"'
  }
}).catch(() => process.exit(1))

Key Concepts

📥 Entry Points

// Single entry point
entry: './src/index.js'

// Multiple entry points
entry: {
  app: './src/app.js',
  admin: './src/admin.js',
  vendor: ['react', 'lodash']
}

📤 Output

output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].[contenthash].js', // Cache busting
  publicPath: '/assets/',
  clean: true // Cleans the output folder
}

Loaders (Webpack)

module: {
  rules: [
    // JavaScript/TypeScript
    {
      test: /\.(js|ts)x?$/,
      exclude: /node_modules/,
      use: 'babel-loader'
    },
    
    // CSS/SCSS
    {
      test: /\.s?css$/,
      use: [
        'style-loader',
        'css-loader',
        'sass-loader'
      ]
    },
    
    // Images
    {
      test: /\.(png|jpg|gif|svg)$/,
      type: 'asset/resource',
      generator: {
        filename: 'images/[name].[hash][ext]'
      }
    },
    
    // Fonts
    {
      test: /\.(woff|woff2|eot|ttf|otf)$/,
      type: 'asset/resource',
      generator: {
        filename: 'fonts/[name].[hash][ext]'
      }
    }
  ]
}

🔌 Plugins

plugins: [
  // Generates the HTML
  new HtmlWebpackPlugin({
    template: './public/index.html',
    minify: true
  }),
  
  // Extracts the CSS
  new MiniCssExtractPlugin({
    filename: '[name].[contenthash].css'
  }),
  
  // Environment variables
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify('production')
  }),
  
  // Bundle analysis
  new BundleAnalyzerPlugin({
    analyzerMode: 'static',
    openAnalyzer: false
  })
]

Code Splitting

// Automatic splitting
optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      // Vendors (node_modules)
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        chunks: 'all'
      },
      
      // Common code
      common: {
        minChunks: 2,
        chunks: 'all',
        name: 'common'
      }
    }
  }
}

// Dynamic imports (lazy loading)
const LazyComponent = lazy(() => import('./LazyComponent'))

// Webpack magic comments
const utils = import(
  /* webpackChunkName: "utils" */
  /* webpackPreload: true */
  './utils'
)

🌳 Tree Shaking

// package.json - Mark as side-effect free
{
  "sideEffects": false
}

// Or specify files with side effects
{
  "sideEffects": [
    "*.css",
    "src/polyfills.js"
  ]
}

// Specific import for tree shaking
import { debounce } from 'lodash' // Imports all of lodash
import debounce from 'lodash/debounce' // Imports only debounce

vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'

export default defineConfig({
  plugins: [
    react({
      // Fast Refresh
      fastRefresh: true
    })
  ],
  
  // Path aliases
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@utils': resolve(__dirname, 'src/utils'),
      '@assets': resolve(__dirname, 'src/assets')
    }
  },
  
  // Environment variables
  define: {
    __APP_VERSION__: JSON.stringify(process.env.npm_package_version)
  },
  
  // Dev server configuration
  server: {
    port: 3000,
    open: true,
    cors: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  
  // Build configuration
  build: {
    outDir: 'dist',
    sourcemap: true,
    minify: 'terser',
    
    // Chunk optimization
    rollupOptions: {
      output: {
        manualChunks: {
          // Vendor chunks
          react: ['react', 'react-dom'],
          router: ['react-router-dom'],
          ui: ['@mui/material', '@emotion/react'],
          utils: ['lodash', 'date-fns', 'axios']
        }
      }
    },
    
    // Terser configuration
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  
  // Dependency optimization
  optimizeDeps: {
    include: ['react', 'react-dom'],
    exclude: ['@vite/client', '@vite/env']
  },
  
  // CSS
  css: {
    modules: {
      localsConvention: 'camelCase'
    },
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
})

package.json Scripts

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "analyze": "vite-bundle-analyzer",
    
    "webpack:dev": "webpack serve --mode development",
    "webpack:build": "webpack --mode production",
    "webpack:analyze": "webpack-bundle-analyzer dist/stats.json",
    
    "rollup:build": "rollup -c",
    "rollup:watch": "rollup -c -w",
    
    "parcel:dev": "parcel src/index.html",
    "parcel:build": "parcel build src/index.html"
  }
}

Advanced Optimizations

Performance

// Lazy loading of routes
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))

// Critical preloading
const criticalData = import(
  /* webpackPreload: true */
  './critical-data'
)

// Prefetching for later
const nonCritical = import(
  /* webpackPrefetch: true */
  './non-critical'
)

Bundle Analysis

# Webpack Bundle Analyzer
npm install -D webpack-bundle-analyzer
npx webpack-bundle-analyzer dist/stats.json

# Vite Bundle Analyzer
npm install -D vite-bundle-analyzer
npx vite-bundle-analyzer

# Rollup Plugin Visualizer
npm install -D rollup-plugin-visualizer

Multi-Environment Configuration

// vite.config.js
export default defineConfig(({ command, mode }) => {
  const isProduction = mode === 'production'
  
  return {
    plugins: [
      react(),
      ...(isProduction ? [
        // Production plugins only
      ] : [])
    ],
    
    build: {
      minify: isProduction ? 'terser' : false,
      sourcemap: !isProduction
    },
    
    define: {
      __DEV__: !isProduction
    }
  }
})

Practical Tips

Best Practices

  • Choose according to the project - Vite for new projects, Webpack for legacy
  • Optimize chunks - Separate vendor, common, and pages
  • Use cache - Contenthash for cache busting
  • Analyze regularly - Monitor bundle size
  • Lazy loading - Load components on demand

Pitfalls to Avoid

  • Over-optimization - Do not optimize prematurely
  • Too large bundles - Monitor size (< 250KB initial)
  • Too many chunks - Avoid excessive fragmentation
  • Unnecessary dependencies - Audit regularly

Debugging

// Source maps for debugging
devtool: 'eval-source-map', // Dev
devtool: 'source-map', // Production

// Verbose logging
stats: 'verbose',

// Performance analysis
performance: {
  hints: 'warning',
  maxEntrypointSize: 250000,
  maxAssetSize: 250000
}

Migration Between Bundlers

Webpack → Vite

// Webpack
module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
}

// Vite equivalent
export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})

Resources for Further Learning