DevToolBoxGRATIS
Blog

Webpack Guide: Loaders, Plugins, Code Splitting, and Module Federation

14 min readoleh DevToolBox
TL;DR

Webpack is a powerful module bundler that builds a dependency graph from your source files and outputs optimized bundles. Key concepts: entry (start point), output (bundle destination), loaders (file transformers), plugins (build lifecycle hooks), and mode (development vs production optimizations).

Key Takeaways
  • Webpack 5 builds a dependency graph from entry points and outputs optimized bundles
  • Loaders transform individual files (TypeScript, SASS, images) before bundling
  • Plugins hook into the build lifecycle for HTML generation, CSS extraction, and optimization
  • Code splitting via dynamic imports and SplitChunksPlugin reduces initial load time
  • Module Federation enables micro-frontend architectures with runtime code sharing
  • Production mode enables tree shaking, minification, and contenthash-based caching
  • Persistent filesystem caching reduces incremental build times by 80-95%

1. What Is Webpack — The Module Bundler Explained

Webpack is a static module bundler for modern JavaScript applications. When Webpack processes your project, it internally builds a dependency graph starting from one or more entry points. It then maps every module your project needs and packages them into one or more bundles — optimized files that can be served by a browser.

Unlike task runners (Gulp, Grunt) that execute sequential transformations, Webpack understands the relationships between modules. This graph-awareness unlocks code splitting, tree shaking, lazy loading, and Module Federation — capabilities that are impossible with naive concatenation.

The five core concepts every Webpack user must understand are:

  • Entry — The starting point of the dependency graph (typically src/index.tsx)
  • Output — Where to emit the bundles and what to name them
  • Loaders — Functions that transform files before they are added to the graph (e.g., TypeScript → JavaScript)
  • Plugins — Extensions that tap into the build lifecycle to perform broader tasks
  • Mode — Either development or production, enabling built-in optimizations
// Conceptual view of the Webpack pipeline:

Entry (src/index.tsx)
  └─ import React from "react"          → node_modules/react/index.js
  └─ import App from "./App"            → src/App.tsx
      └─ import styles from "./App.css" → src/App.css
      └─ import logo from "./logo.svg"  → src/logo.svg

Webpack builds the graph, applies loaders, runs plugins,
then emits:
  dist/main.a1b2c3.js     (app code + dependencies)
  dist/main.a1b2c3.css    (extracted CSS via MiniCssExtractPlugin)
  dist/logo.d4e5f6.svg    (hashed asset)
  dist/index.html         (generated by HtmlWebpackPlugin)

Webpack originated in 2012 and has been through five major versions. Webpack 5 (released October 2020) introduced persistent caching, Module Federation, built-in asset modules, and improved tree shaking — all of which remain central to its use in 2026.

2. Core Configuration — Entry, Output, Resolve, Mode, and Devtool

The webpack.config.js file exports a configuration object (or a function returning one). Understanding the top-level keys is essential before adding loaders and plugins.

// webpack.config.js — Core configuration explained
const path = require('path');

module.exports = {
  // ─── MODE ───────────────────────────────────────────────
  // "development": enables useful error messages, source maps,
  //               disables minification for readable output
  // "production": enables minification, tree shaking, scope hoisting
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',

  // ─── ENTRY ───────────────────────────────────────────────
  // Single entry: one output chunk
  entry: './src/index.tsx',

  // Multiple entries: named output chunks
  // entry: {
  //   main: './src/index.tsx',
  //   admin: './src/admin/index.tsx',
  // },

  // ─── OUTPUT ──────────────────────────────────────────────
  output: {
    path: path.resolve(__dirname, 'dist'),
    // [contenthash] changes only when file content changes
    // Enables long-term browser caching
    filename: '[name].[contenthash:8].js',
    // Clean output directory before each build
    clean: true,
    // Public path for CDN deployments
    publicPath: '/',
  },

  // ─── RESOLVE ─────────────────────────────────────────────
  resolve: {
    // Try these extensions in order when importing without extension
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
    // Aliases for cleaner imports
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
    },
  },

  // ─── DEVTOOL ─────────────────────────────────────────────
  // Source map strategy — affects build speed and debuggability
  // Development: "eval-source-map" (fast rebuilds, full source maps)
  // Production: "source-map" (external .map files, full mapping)
  //             "hidden-source-map" (for error monitoring, not public)
  devtool: process.env.NODE_ENV === 'production' ? 'source-map' : 'eval-source-map',

  // ─── CACHE ───────────────────────────────────────────────
  // Webpack 5 persistent caching — stores compilation to disk
  // Reduces incremental build times by 80-95%
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename], // Invalidate cache when config changes
    },
  },
};

The contenthash pattern in output filenames is critical for production deployments. When file content does not change, the hash stays the same, so browsers continue to serve the cached file. Only changed files get new hashes, forcing browsers to fetch updated content.

The devtool option trades build speed for source map quality. In development, use eval-source-map for fast incremental rebuilds with accurate source locations. In production, use source-map to generate separate .map files that can be uploaded to error monitoring services like Sentry without shipping them to end users.

3. Loaders — Transforming Every File Type

Loaders are the translation layer between your source files and the Webpack module graph. They are functions that receive a file's source content and return transformed JavaScript (or another format Webpack can process). Loaders are configured as rules that match files by regex pattern.

Important: Loaders in an array are applied right to left (or bottom to top). So ['style-loader', 'css-loader', 'postcss-loader'] first runs postcss-loader, then css-loader, then style-loader.

// webpack.config.js — module.rules (loaders configuration)
module.exports = {
  module: {
    rules: [

      // ─── TYPESCRIPT / JAVASCRIPT ─────────────────────────
      {
        test: /\.(tsx?|jsx?)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', { targets: '> 0.5%, not dead' }],
              ['@babel/preset-react', { runtime: 'automatic' }],
              '@babel/preset-typescript',
            ],
            // Caches transpilation results to disk for faster rebuilds
            cacheDirectory: true,
          },
        },
      },

      // Alternative: ts-loader (uses tsc, full type checking)
      // {
      //   test: /\.tsx?$/,
      //   use: 'ts-loader',
      //   exclude: /node_modules/,
      // },

      // ─── CSS ─────────────────────────────────────────────
      // Development: style-loader injects CSS via <style> tags (enables HMR)
      // Production: MiniCssExtractPlugin.loader extracts to .css files
      {
        test: /\.css$/,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              modules: {
                // Enable CSS Modules for *.module.css files
                auto: /\.module\.css$/,
                localIdentName: isDev ? '[local]--[hash:base64:5]' : '[hash:base64:8]',
              },
              importLoaders: 1, // Number of loaders before css-loader
            },
          },
          'postcss-loader', // Handles autoprefixer, tailwind, etc.
        ],
      },

      // ─── SASS / SCSS ──────────────────────────────────────
      {
        test: /\.(scss|sass)$/,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'sass-loader', // Compiles Sass to CSS (requires sass package)
        ],
      },

      // ─── IMAGES & FONTS (Webpack 5 Asset Modules) ─────────
      // Replaces file-loader and url-loader from Webpack 4
      {
        test: /\.(png|jpg|jpeg|gif|webp)$/i,
        type: 'asset',
        parser: {
          // Inline as base64 if < 8KB, otherwise emit as file
          dataUrlCondition: { maxSize: 8 * 1024 },
        },
        generator: {
          filename: 'images/[name].[contenthash:8][ext]',
        },
      },
      {
        test: /\.svg$/,
        // Use 'asset/source' to inline SVG as raw string
        // Or @svgr/webpack to import SVG as React components
        use: ['@svgr/webpack'],
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name].[contenthash:8][ext]',
        },
      },

    ],
  },
};

babel-loader vs ts-loader: babel-loader strips TypeScript types without performing type-checking, making it significantly faster. Use it with a separate tsc --noEmit in CI for type safety. ts-loader runs the TypeScript compiler and catches type errors during the build but is slower.

Webpack 5 Asset Modules replace the three old file loaders: file-loader (emit file → asset/resource), url-loader (inline as base64 → asset/inline), and raw-loader (source as string → asset/source). The asset type automatically chooses between inline and file based on a configurable size threshold.

4. Plugins — Extending the Build Lifecycle

While loaders operate on individual files, plugins tap into Webpack's compilation lifecycle to perform tasks that span the entire build. They can generate files, inject environment variables, analyze bundle sizes, or copy static assets.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { DefinePlugin } = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [

    // ─── HtmlWebpackPlugin ───────────────────────────────────
    // Generates dist/index.html with correct <script> and <link> tags
    // automatically updated with hashed filenames
    new HtmlWebpackPlugin({
      template: './public/index.html',   // Base HTML template
      favicon: './public/favicon.ico',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true,
      },
    }),

    // ─── MiniCssExtractPlugin ────────────────────────────────
    // Extracts CSS into separate files for parallel loading
    // Required for production (replaces style-loader)
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css',
    }),

    // ─── DefinePlugin ────────────────────────────────────────
    // Replaces variables at compile time — enables dead code elimination
    // Values must be JSON.stringify('d strings for string literals
    new DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'process.env.API_BASE_URL': JSON.stringify(process.env.API_BASE_URL),
      __DEV__: process.env.NODE_ENV !== 'production',
      APP_VERSION: JSON.stringify(require('./package.json').version),
    }),

    // ─── CopyWebpackPlugin ───────────────────────────────────
    // Copies files/directories to output — useful for static assets
    // not imported by any module
    new CopyWebpackPlugin({
      patterns: [
        { from: 'public/robots.txt', to: 'robots.txt' },
        { from: 'public/images', to: 'images' },
      ],
    }),

    // ─── BundleAnalyzerPlugin ────────────────────────────────
    // Opens an interactive treemap of your bundle contents
    // Enable only when analyzing: ANALYZE=true npm run build
    ...(process.env.ANALYZE ? [new BundleAnalyzerPlugin()] : []),

  ],
};

The DefinePlugin is particularly powerful because it performs compile-time string replacement, not runtime variable injection. When you define process.env.NODE_ENV as "production", Webpack replaces every occurrence of that expression in your code with the literal string. Any code inside if (process.env.NODE_ENV !== 'production') { ... } blocks becomes dead code that tree shaking removes.

5. Code Splitting — Dynamic Imports, SplitChunksPlugin, and Bundle Analysis

Code splitting is one of Webpack's most impactful optimization features. Instead of shipping one massive bundle, you split your code into smaller chunks that load on demand. This reduces Time to Interactive (TTI) by only loading what the current page actually needs.

There are three approaches to code splitting in Webpack:

  • Multiple entry points — manually define multiple bundles (limited and error-prone)
  • Dynamic imports — use import() to tell Webpack where to split
  • SplitChunksPlugin — automatically extract shared modules into separate chunks
// Dynamic imports — the primary code splitting mechanism

// React lazy loading (most common pattern)
import React, { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() =>
  // Webpack magic comment: set the chunk name
  import(/* webpackChunkName: "analytics" */ './pages/Analytics')
);
const Settings = lazy(() =>
  import(
    /* webpackChunkName: "settings" */
    /* webpackPrefetch: true */  // Hint browser to prefetch after idle
    './pages/Settings'
  )
);

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
// webpack.config.js — SplitChunksPlugin configuration
module.exports = {
  optimization: {
    splitChunks: {
      // Split both async (dynamic imports) and sync chunks
      chunks: 'all',

      // Minimum size (bytes) before a chunk is generated
      minSize: 20_000,   // 20 KB

      // Maximum size (bytes) — Webpack tries to split larger chunks
      maxSize: 244_000,  // ~244 KB (HTTP/2 sweet spot)

      // Minimum number of chunks that must share a module before splitting
      minChunks: 1,

      cacheGroups: {
        // Separate vendor libraries (node_modules) into their own chunk
        // Vendors rarely change so the browser caches them long-term
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 20,
          reuseExistingChunk: true,
        },
        // Split React core into its own tiny chunk
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react-vendor',
          priority: 30, // Higher priority wins
        },
        // Split shared internal utilities used in 2+ chunks
        common: {
          name: 'common',
          minChunks: 2,
          priority: 10,
          reuseExistingChunk: true,
        },
      },
    },
    // Generate a separate runtime chunk for better long-term caching
    runtimeChunk: 'single',
  },
};

Bundle analysis is essential for understanding what ends up in your output. Run ANALYZE=true npm run build with webpack-bundle-analyzer configured to open an interactive treemap. Look for: large node_modules that should be lazily loaded, duplicate copies of the same library at different versions, and modules imported in many places that could be extracted.

6. Dev Server and Hot Module Replacement (HMR)

webpack-dev-server provides a local HTTP server with automatic reloading during development. Hot Module Replacement (HMR) goes further — it replaces updated modules in a running application without a full page reload, preserving application state.

// webpack.config.js — devServer configuration
module.exports = {
  devServer: {
    // Serve from dist output directory
    static: {
      directory: path.join(__dirname, 'public'),
    },

    // Enable Hot Module Replacement
    hot: true,

    // Enable history API fallback for SPAs (React Router)
    historyApiFallback: true,

    port: 3000,
    open: true, // Open browser on start

    // Proxy API requests to backend server
    // Avoids CORS issues during development
    proxy: [
      {
        context: ['/api'],
        target: 'http://localhost:8080',
        changeOrigin: true,
        // Rewrite paths: /api/users → /users
        pathRewrite: { '^/api': '' },
      },
    ],

    // Compress responses with gzip
    compress: true,

    // Show full-screen overlay for errors
    client: {
      overlay: {
        errors: true,
        warnings: false,
      },
      progress: true,
    },

    // Custom headers (useful for SharedArrayBuffer / COOP / COEP)
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
};
// HMR setup for React — using react-refresh
// Install: npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
  // In dev mode only:
  plugins: [
    isDev && new ReactRefreshWebpackPlugin(),
  ].filter(Boolean),

  module: {
    rules: [
      {
        test: /\.(tsx?|jsx?)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            plugins: [
              // Only add react-refresh/babel in development
              isDev && require.resolve('react-refresh/babel'),
            ].filter(Boolean),
          },
        },
      },
    ],
  },
};

// Manual HMR acceptance (for non-React modules)
// In your module:
if (module.hot) {
  module.hot.accept('./state', () => {
    // Re-import and apply updated module
    const nextState = require('./state').default;
    applyState(nextState);
  });
}

HMR vs live reload: Live reload refreshes the entire browser page when any file changes. HMR surgically replaces only the changed module and its dependents, preserving React component state, form data, scroll positions, and other runtime state. This dramatically improves the feedback loop during development.

7. Production Optimization — Tree Shaking, Minification, Terser, and Caching

When mode is set to "production", Webpack enables many optimizations automatically. But there are additional configurations that can reduce bundle size by an additional 20-40% on top of defaults.

// webpack.config.js — Full production optimization config
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  mode: 'production',

  optimization: {
    // ─── MINIFICATION ──────────────────────────────────────
    minimize: true,
    minimizer: [
      // TerserPlugin: minifies JS
      new TerserPlugin({
        parallel: true,  // Use multi-process parallel running
        terserOptions: {
          compress: {
            drop_console: true,   // Remove console.log in production
            drop_debugger: true,  // Remove debugger statements
            pure_funcs: ['console.log', 'console.info'],
          },
          format: {
            comments: false,      // Remove all comments
          },
        },
        extractComments: false,   // Don't emit license.txt files
      }),
      // CssMinimizerPlugin: minifies extracted CSS
      new CssMinimizerPlugin(),
    ],

    // ─── TREE SHAKING ─────────────────────────────────────
    // usedExports: Mark unused exports (requires ES modules)
    usedExports: true,
    // sideEffects: Respect package.json sideEffects field
    sideEffects: true,

    // ─── SCOPE HOISTING ────────────────────────────────────
    // ModuleConcatenationPlugin: merge ES modules into single scope
    // Reduces bundle size and improves runtime performance
    // Enabled automatically in production mode
    concatenateModules: true,

    // ─── CHUNK NAMING ──────────────────────────────────────
    // Use deterministic IDs instead of auto-incrementing numbers
    // Ensures chunk IDs don't change when unrelated modules are added
    moduleIds: 'deterministic',
    chunkIds: 'deterministic',

    // ─── SPLIT CHUNKS ─────────────────────────────────────
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
    runtimeChunk: 'single',
  },
};

Tree shaking requirements: For tree shaking to work, three conditions must be met: (1) your code must use ES module syntax (import/export, not require), (2) the library you import from must also use ES modules (check its package.json for a module field), and (3) the module must declare sideEffects: false in its package.json.

// package.json — sideEffects configuration for your library
{
  "name": "my-ui-library",
  // Tell Webpack/bundlers that no modules have side effects
  // This enables aggressive tree shaking
  "sideEffects": false,

  // Exception: CSS files always have side effects
  // "sideEffects": ["*.css", "*.scss"],

  "main": "dist/index.cjs.js",    // CommonJS
  "module": "dist/index.esm.js",  // ES Modules (preferred for bundlers)
  "exports": {
    "import": "./dist/index.esm.js",
    "require": "./dist/index.cjs.js"
  }
}

8. Webpack 5 Module Federation — Micro-Frontends at Scale

Module Federation is the flagship feature of Webpack 5. It allows multiple separately compiled and deployed Webpack applications to share code at runtime — no shared build step, no monorepo required. This is the architecture powering large-scale micro-frontend systems at companies like Zalando, IKEA, and many others.

The model has two roles: a host application that consumes remote modules, and remote applications that expose modules. A single application can be both a host and a remote simultaneously.

// Remote Application — webpack.config.js
// This app EXPOSES components for other apps to consume

const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // Unique name for this federated module
      name: "productApp",

      // The entry point file that other apps will load
      // Accessible at: https://products.example.com/remoteEntry.js
      filename: "remoteEntry.js",

      // What this app exposes to others
      exposes: {
        "./ProductList": "./src/components/ProductList",
        "./ProductCard": "./src/components/ProductCard",
        "./useCart": "./src/hooks/useCart",
      },

      // Shared dependencies — prevents duplicate instances
      // Both apps will share a single React instance
      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
      },
    }),
  ],
};
// Host Application — webpack.config.js
// This app CONSUMES components from remote apps

const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "shell",

      // Register remote applications
      remotes: {
        // Format: "name@url/remoteEntry.js"
        productApp: "productApp@https://products.example.com/remoteEntry.js",
        cartApp: "cartApp@https://cart.example.com/remoteEntry.js",
      },

      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
      },
    }),
  ],
};

// ─── Consuming remote modules in your React code ───────────────
// src/App.tsx (in the host application)

import React, { lazy, Suspense } from "react";

// Remote components load asynchronously via the remoteEntry.js script
const ProductList = lazy(() => import("productApp/ProductList"));
const CartWidget = lazy(() => import("cartApp/CartWidget"));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading products...</div>}>
        <ProductList />
      </Suspense>
      <Suspense fallback={<div>Loading cart...</div>}>
        <CartWidget />
      </Suspense>
    </div>
  );
}

The shared configuration is critical. Without it, each federated app would load its own copy of React, leading to multiple React instances that break hooks and context. The singleton: true option ensures all federated modules share a single instance. The requiredVersion field triggers a runtime warning if version requirements are not met.

Bootstrap pattern: Module Federation requires that the entry point be asynchronous. Wrap your application bootstrap in a dynamic import:

// src/index.tsx (host or remote)
// Without this async bootstrap, Module Federation fails with:
// "Shared module is not available for eager consumption"

import(/* webpackChunkName: "bootstrap" */ "./bootstrap");

// src/bootstrap.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(<App />);

9. Webpack vs Vite vs esbuild vs Rollup vs Parcel — Comparison Table

Choosing the right bundler depends on your project's requirements. Here is a comprehensive comparison of the five major bundlers in 2026:

Feature / ToolWebpack 5Vite 6esbuildRollup 4Parcel 2
Primary use caseLarge apps, micro-frontendsModern SPAs, full-stack appsUltra-fast builds, toolingLibraries, ESM bundlesZero-config apps
Dev server cold start8–18s200–600ms~50msN/A (no dev server)2–5s
HMR speed200–800ms10–50ms~50msN/A50–200ms
Production bundlerWebpack (JS)Rollup / Rolldownesbuild (Go)Rollup (JS)SWC (Rust)
Code splittingExcellentExcellentBasicGoodGood
Tree shakingExcellentExcellentGoodExcellentGood
Module FederationNativePlugin onlyNoNoNo
TypeScript supportVia ts-loader / babelBuilt-in (esbuild)Built-inPluginBuilt-in
CSS ModulesVia css-loaderBuilt-inBasicPluginBuilt-in
SSR supportManualFirst-classManualManualExperimental
Legacy browser supportbabel-loader@vitejs/legacy pluginLimitedPluginBuilt-in
Configuration complexityHigh (80–300 lines)Low (10–30 lines)Low (JS API)MediumNone (zero-config)
Plugin ecosystemThousandsGrowing (500+)LimitedHundredsModerate
Best forEnterprise, micro-frontendsNew projects 2026Build tools, CLIsLibrary authorsPrototypes, beginners

When to choose Webpack in 2026: If you need Module Federation for micro-frontends, are maintaining a large existing Webpack codebase, need IE11 support, or depend on specific Webpack-only loaders. For new applications without these requirements, Vite is the recommended default.

When to choose Rollup: If you are building a JavaScript library or component library intended for publishing on npm. Rollup produces the cleanest ESM output with minimal boilerplate, ideal for tree-shaking-friendly packages.

When to choose esbuild: If you are building internal tooling, scripts, or a CLI tool where build speed is the top priority and plugin ecosystem depth is less important.

10. Complete Production-Ready Webpack 5 Configuration

Here is a battle-tested, production-ready webpack configuration that incorporates all the best practices covered in this guide. It handles TypeScript, React, CSS Modules, SASS, image optimization, code splitting, and environment-based configuration.

// webpack.config.js — Production-ready configuration
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { DefinePlugin } = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const isDev = process.env.NODE_ENV !== 'production';

module.exports = {
  mode: isDev ? 'development' : 'production',

  entry: './src/index.tsx',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: isDev ? 'js/[name].js' : 'js/[name].[contenthash:8].js',
    chunkFilename: isDev ? 'js/[name].chunk.js' : 'js/[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
    publicPath: '/',
    clean: true,
  },

  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
    alias: { '@': path.resolve(__dirname, 'src') },
  },

  devtool: isDev ? 'eval-source-map' : 'source-map',

  cache: {
    type: 'filesystem',
    buildDependencies: { config: [__filename] },
  },

  module: {
    rules: [
      {
        test: /\.(tsx?|jsx?)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', { targets: '> 0.5%, not dead' }],
              ['@babel/preset-react', { runtime: 'automatic' }],
              '@babel/preset-typescript',
            ],
            plugins: [isDev && 'react-refresh/babel'].filter(Boolean),
            cacheDirectory: true,
          },
        },
      },
      {
        test: /\.css$/,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          { loader: 'css-loader', options: { modules: { auto: true } } },
          'postcss-loader',
        ],
      },
      {
        test: /\.(scss|sass)$/,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader', 'postcss-loader', 'sass-loader',
        ],
      },
      {
        test: /\.(png|jpg|jpeg|gif|webp)$/i,
        type: 'asset',
        parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
      },
      { test: /\.svg$/, use: ['@svgr/webpack'] },
      { test: /\.(woff2?|eot|ttf|otf)$/, type: 'asset/resource' },
    ],
  },

  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html', favicon: './public/favicon.ico' }),
    !isDev && new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' }),
    new DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'process.env.API_URL': JSON.stringify(process.env.API_URL || '/api'),
    }),
    new CopyWebpackPlugin({ patterns: [{ from: 'public/robots.txt' }] }),
    isDev && new ReactRefreshWebpackPlugin(),
  ].filter(Boolean),

  optimization: {
    minimize: !isDev,
    minimizer: [
      new TerserPlugin({ terserOptions: { compress: { drop_console: true } } }),
      new CssMinimizerPlugin(),
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 20 },
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
          name: 'react-vendor',
          priority: 30,
        },
      },
    },
    runtimeChunk: 'single',
    moduleIds: 'deterministic',
  },

  devServer: {
    port: 3000,
    hot: true,
    historyApiFallback: true,
    proxy: [{ context: ['/api'], target: 'http://localhost:8080', changeOrigin: true }],
  },
};

Install all required dependencies with this single command:

npm install --save-dev \
  webpack webpack-cli webpack-dev-server \
  babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript \
  css-loader style-loader postcss-loader postcss autoprefixer \
  sass sass-loader \
  mini-css-extract-plugin css-minimizer-webpack-plugin \
  html-webpack-plugin copy-webpack-plugin \
  terser-webpack-plugin \
  @svgr/webpack \
  @pmmmwh/react-refresh-webpack-plugin react-refresh \
  webpack-bundle-analyzer

Frequently Asked Questions

What is Webpack and how does it differ from task runners like Gulp?

Webpack is a static module bundler — it builds a complete dependency graph of your application starting from one or more entry points, then packages everything into optimized output bundles. Gulp and Grunt are task runners that execute sequential tasks (minify, compile, copy) but lack the module graph awareness that makes Webpack powerful for code splitting, tree shaking, and lazy loading.

What is the difference between Webpack 4 and Webpack 5?

Webpack 5 (released 2020) introduced: persistent filesystem caching (dramatically faster incremental builds), Module Federation (runtime code sharing between separate builds), built-in asset modules replacing file-loader/url-loader, improved tree shaking with nested exports, and automatic node.js polyfill removal. Webpack 5 also removed deprecated APIs and enforced stricter module resolution.

How do I enable persistent caching in Webpack 5 to speed up builds?

Add cache: { type: "filesystem" } to your webpack.config.js. This caches the compilation to disk and can reduce rebuild times by 80-95% on subsequent builds. The cache is stored in node_modules/.cache/webpack by default. Combine with babel-loader cacheDirectory: true for even faster incremental builds.

What is tree shaking in Webpack and how do I enable it?

Tree shaking eliminates dead code (exports that are never imported) from your bundles. It works automatically when: (1) you use ES modules (import/export, not require/module.exports), (2) mode is set to "production", and (3) the sideEffects field is set in package.json. Mark pure modules with sideEffects: false in their package.json to allow Webpack to safely remove unused exports.

What is the SplitChunksPlugin and how should I configure it?

SplitChunksPlugin automatically splits shared modules into separate chunks. The default configuration handles most cases. Use chunks: "all" to split async and sync modules, set minSize and maxSize to control chunk sizes, and use cacheGroups to define custom splitting rules. A common pattern is separating vendor (node_modules) code into its own chunk using cacheGroups.vendors.

How does Webpack Module Federation work for micro-frontends?

Module Federation allows separate Webpack builds to share code at runtime without a shared build step. A host application consumes remote modules exposed by other applications. Configure the ModuleFederationPlugin with name (unique app ID), filename (usually remoteEntry.js), exposes (what this app shares), and remotes (what it consumes). Remote modules are loaded asynchronously via script tags at runtime.

What is the difference between style-loader and MiniCssExtractPlugin?

style-loader injects CSS into the DOM as <style> tags at runtime — ideal for development because it enables Hot Module Replacement for CSS changes. MiniCssExtractPlugin extracts CSS into separate .css files — required for production because it enables parallel CSS loading, browser caching of CSS separately from JS, and eliminates the flash of unstyled content caused by JS-injected styles.

When should I migrate from Webpack to Vite?

Consider migrating if: dev server startup takes more than 10 seconds, HMR is slow (>500ms), or the team is spending significant time maintaining webpack config. Stick with Webpack if: you use Module Federation for micro-frontends, have IE11 requirements, depend on Webpack-specific loaders, or have a stable complex config that works well. Vite is the recommended default for new projects in 2026.

Related Tools and Resources

𝕏 Twitterin LinkedIn
Apakah ini membantu?

Tetap Update

Dapatkan tips dev mingguan dan tool baru.

Tanpa spam. Berhenti kapan saja.

Coba Alat Terkait

{ }JSON Formatter{ }CSS Minifier / Beautifier

Artikel Terkait

Web Performance Optimization Guide: Core Web Vitals, Caching, and React/Next.js

Master web performance optimization. Covers Core Web Vitals (LCP, FID, CLS), image optimization, code splitting, caching strategies, React/Next.js performance, Lighthouse scoring, and real-world benchmarks.

Panduan Lengkap React Hooks: useState, useEffect, dan Custom Hooks

Kuasai React Hooks dengan contoh praktis. Pelajari useState, useEffect, useContext, useReducer, useMemo, useCallback, custom hooks, dan React 18+ concurrent hooks.

Node.js Guide: Complete Tutorial for Backend Development

Master Node.js backend development. Covers event loop, Express.js, REST APIs, authentication with JWT, database integration, testing with Jest, PM2 deployment, and Node.js vs Deno vs Bun comparison.