Skip to content

Webpack 5 Reference: Config, Loaders, Code Splitting, Dev Server & Module Federation

Webpack 5 is still the most widely installed JavaScript bundler. Most large production apps, Create React App projects, and enterprise React/Angular/Vue builds run on webpack. The biggest v5 changes: Module Federation (share code across separately deployed apps), persistent disk cache (massively faster rebuilds), and asset modules replacing url-loader/file-loader. The config surface is large — this reference covers the patterns you actually need.

1. Core Config — Entry, Output, Mode

webpack.config.js structure, entry points, output, mode, and environment-specific configs
// webpack.config.js (or .ts with ts-node):
const path = require("path");
const { DefinePlugin } = require("webpack");

module.exports = (env, argv) => {
  const isProd = argv.mode === "production";

  return {
    mode: isProd ? "production" : "development",  // "none" | "development" | "production"
    // production: enables minification, tree-shaking, scope hoisting automatically
    // development: enables HMR, better error messages, eval source maps

    entry: "./src/index.tsx",
    // Multiple entry points:
    // entry: { main: "./src/index.tsx", admin: "./src/admin.tsx" }

    output: {
      path: path.resolve(__dirname, "dist"),
      filename: isProd ? "[name].[contenthash].js" : "[name].js",
      // contenthash changes only when file content changes — long-term caching
      chunkFilename: isProd ? "[id].[contenthash].js" : "[id].js",
      assetModuleFilename: "assets/[hash][ext]",
      clean: true,   // clean dist/ before each build (replaces CleanWebpackPlugin)
      publicPath: "/",  // base URL for all assets in the HTML
    },

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

    plugins: [
      new DefinePlugin({
        "process.env.NODE_ENV": JSON.stringify(isProd ? "production" : "development"),
        "__VERSION__": JSON.stringify(process.env.npm_package_version),
      }),
    ],
  };
};

2. Loaders — TypeScript, CSS, Assets

babel-loader for JS/TS, css-loader + style-loader, CSS Modules, and asset modules
// npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
// npm install -D css-loader style-loader postcss-loader autoprefixer

module.exports = {
  module: {
    rules: [
      // TypeScript + JSX via Babel (faster than ts-loader, no type-check):
      {
        test: /\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              ["@babel/preset-env", { targets: "defaults" }],
              ["@babel/preset-react", { runtime: "automatic" }],  // no React import needed
              "@babel/preset-typescript",  // strips types (no checking)
            ],
          },
        },
      },

      // CSS + PostCSS (global styles):
      {
        test: /\.css$/,
        exclude: /\.module\.css$/,
        use: [
          isProd ? MiniCssExtractPlugin.loader : "style-loader",  // extract in prod, inject in dev
          "css-loader",
          { loader: "postcss-loader", options: { postcssOptions: { plugins: ["autoprefixer"] } } },
        ],
      },

      // CSS Modules (*.module.css):
      {
        test: /\.module\.css$/,
        use: ["style-loader", { loader: "css-loader", options: { modules: true } }],
      },

      // Asset modules (webpack 5 — replaces url-loader + file-loader):
      { test: /\.(png|jpg|gif|webp)$/, type: "asset/resource" },   // copy to output
      { test: /\.svg$/,               type: "asset/source" },      // inline as string
      { test: /\.(woff2?|eot|ttf)$/, type: "asset/resource" },     // fonts
      {
        test: /\.(png|jpg)$/,
        type: "asset",   // auto: inline if < 8kb, file if >= 8kb
        parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
      },
    ],
  },
};

3. Code Splitting, Optimization & Tree Shaking

Dynamic imports, SplitChunksPlugin, tree shaking requirements, and bundle analysis
// Dynamic import — automatic code splitting:
const Dashboard = React.lazy(() => import("./pages/Dashboard"));
// webpack creates a separate chunk for Dashboard.tsx

// optimization config:
module.exports = {
  optimization: {
    // Extract vendor chunk (better long-term caching):
    splitChunks: {
      chunks: "all",   // "async" (default, only async) | "all" (sync + async)
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendors",
          chunks: "all",
        },
        // Separate rarely-changing large libs:
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: "react-vendor",
          chunks: "all",
          priority: 20,   // higher priority = matched first
        },
      },
    },
    // Keep runtime in a separate file (prevents hash changes in vendor chunk):
    runtimeChunk: "single",
    // Minification (webpack 5 built-in for JS; CSS needs plugin):
    minimize: isProd,
    minimizer: [
      "...",   // keep default JS minimizer (terser)
      new CssMinimizerPlugin(),  // npm install -D css-minimizer-webpack-plugin
    ],
  },
};

// Tree shaking requirements:
// 1. ES modules (import/export — not require/module.exports)
// 2. "sideEffects": false in package.json (or list files with side effects)
// 3. mode: "production"

// Bundle analysis:
// npm install -D webpack-bundle-analyzer
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
plugins: [new BundleAnalyzerPlugin()]  // opens treemap in browser

4. Dev Server, HMR & Source Maps

webpack-dev-server config, HMR, proxy for API, and source map options
// npm install -D webpack-dev-server

module.exports = {
  devServer: {
    port: 3000,
    open: true,
    hot: true,                  // enable HMR
    historyApiFallback: true,   // serve index.html for all 404s (SPA routing)
    compress: true,             // gzip responses
    static: { directory: path.join(__dirname, "public") },

    // Proxy API requests to avoid CORS:
    proxy: [{
      context: ["/api"],
      target: "http://localhost:8080",
      changeOrigin: true,
    }],

    // Custom headers:
    headers: { "Access-Control-Allow-Origin": "*" },
  },

  // Source maps:
  devtool: isProd
    ? "source-map"              // prod: separate .map files, full quality
    : "eval-cheap-module-source-map",  // dev: fast, good enough for debugging
    // Other options:
    // "inline-source-map"     — embedded in bundle (large, slow)
    // "hidden-source-map"     — external, no URL comment (for error tracking only)
    // false                   — no source maps
};

5. Module Federation & HTML Plugin

HtmlWebpackPlugin for HTML generation, MiniCssExtractPlugin, and Module Federation
// HtmlWebpackPlugin — generates index.html with script tags:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { ModuleFederationPlugin } = require("webpack").container;

plugins: [
  new HtmlWebpackPlugin({
    template: "./public/index.html",   // your HTML template
    favicon: "./public/favicon.ico",
    minify: isProd,
  }),

  // Extract CSS to separate files in production (no flash of unstyled content):
  isProd && new MiniCssExtractPlugin({
    filename: "[name].[contenthash].css",
    chunkFilename: "[id].[contenthash].css",
  }),

  // Module Federation — share code between separately deployed apps (micro-frontends):
  new ModuleFederationPlugin({
    name: "shell",
    remotes: {
      // Load checkout app at runtime from another deployment:
      checkout: "checkout@https://checkout.example.com/remoteEntry.js",
    },
    shared: { react: { singleton: true }, "react-dom": { singleton: true } },
  }),
].filter(Boolean);

// In checkout app:
new ModuleFederationPlugin({
  name: "checkout",
  filename: "remoteEntry.js",   // entry file exposed to shell
  exposes: {
    "./CheckoutWidget": "./src/CheckoutWidget",
    "./Cart":           "./src/Cart",
  },
  shared: { react: { singleton: true }, "react-dom": { singleton: true } },
});

// In shell app code:
const CheckoutWidget = React.lazy(() => import("checkout/CheckoutWidget"));

Track Node.js and build tooling releases at ReleaseRun. Related: Vite 6 Reference | esbuild Reference | TypeScript Reference

🔍 Free tool: npm Package Health Checker — check webpack and its loader/plugin packages for known CVEs and active maintenance.

Founded

2023 in London, UK

Contact

hello@releaserun.com