cheat sheet

webpack

Daily-driver reference for webpack — CLI commands, dev server, loaders, plugins, code splitting, common config recipes.

webpack — CLI & config

What it is

webpack is the classic JS module bundler — loaders, plugins, code splitting, tree shaking, module federation. New apps lean toward Vite or Rspack, but webpack is still the most-deployed bundler in production. This page covers the CLI and config patterns you reach for daily.

For full ecosystem context (versioning, security, migrations), see packages-npm/npm-webpack.

Install

bash
npm install -D webpack webpack-cli
# dev server (separate)
npm install -D webpack-dev-server

Output: webpack and webpack serve available via npx.

Day-to-day commands

CommandWhat it does
webpackBuild with webpack.config.js
webpack --mode productionBuild in prod mode (minify, optimise)
webpack --mode developmentBuild in dev mode (faster, source maps)
webpack --watchRebuild on file change
webpack --config webpack.prod.jsUse a specific config file
webpack --config-name webPick one config from an array of configs
webpack serveRun the dev server with HMR
webpack serve --openSame, auto-open browser
webpack --analyzeOpen webpack-bundle-analyzer report (with plugin)
webpack --profile --json > stats.jsonEmit build stats for analysis

Common scenarios

Minimal config

javascript
// webpack.config.js
const path = require("node:path");

module.exports = {
  mode: "production",
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.[contenthash].js",
    clean: true,
  },
};
bash
npx webpack

Output:

text
asset bundle.f3a9c2d8.js 1.2 KiB [emitted] [immutable]
webpack 5.96.0 compiled successfully in 154 ms

Dev server with HMR

javascript
// webpack.config.js
module.exports = {
  mode: "development",
  entry: "./src/index.js",
  devServer: {
    static: "./dist",
    port: 3000,
    hot: true,
    open: true,
    historyApiFallback: true,        // SPA routing fallback
    proxy: [{ context: ["/api"], target: "http://localhost:4000" }],
  },
};
bash
npx webpack serve

Output:

text
<i> [webpack-dev-server] Project is running at http://localhost:3000/
<i> [webpack-dev-server] webpack compiled successfully

Loader chain — TS + CSS + assets

javascript
module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "swc-loader",            // ~5–10× faster than babel-loader
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader", "postcss-loader"],
      },
      {
        test: /\.(png|jpg|svg|woff2?)$/,
        type: "asset/resource",        // Asset Modules (webpack 5)
      },
    ],
  },
  resolve: { extensions: [".ts", ".tsx", ".js"] },
};

Loaders apply right-to-left: css-loader parses imports, postcss-loader transforms, style-loader injects.

Plugins

javascript
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({ template: "./src/index.html" }),
    new MiniCssExtractPlugin({ filename: "assets/[name].[contenthash].css" }),
  ],
};

Replace style-loader with MiniCssExtractPlugin.loader to emit .css files in production.

Code splitting

javascript
optimization: {
  splitChunks: {
    chunks: "all",
    cacheGroups: {
      react: {
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
        name: "react",
        chunks: "all",
      },
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: "vendor",
        priority: -10,
      },
    },
  },
  runtimeChunk: "single",
}

Dynamic imports get their own chunks:

javascript
const Settings = () => import(/* webpackChunkName: "settings" */ "./Settings");

Multi-target build (browser + Node)

javascript
module.exports = [
  {
    name: "web",
    target: "web",
    entry: "./src/web/index.ts",
    output: { path: __dirname + "/dist/web", filename: "bundle.js" },
  },
  {
    name: "node",
    target: "node",
    entry: "./src/server/index.ts",
    output: { path: __dirname + "/dist/server", filename: "bundle.cjs" },
    externals: ["express"],
  },
];
bash
npx webpack             # builds both
npx webpack --config-name web   # builds only "web"

Output:

text
asset web.js 124 KiB [emitted]
asset node.js 38 KiB [emitted]
webpack 5.94.0 compiled successfully in 1842 ms

Tree shaking

json
// package.json
{
  "sideEffects": ["*.css", "./src/polyfills.js"]
}

sideEffects: false for the whole package lets webpack drop unused exports. An array whitelists files that must remain.

Module Federation

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

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "host",
      remotes: {
        checkout: "checkout@https://checkout.example.com/remoteEntry.js",
      },
      shared: { react: { singleton: true }, "react-dom": { singleton: true } },
    }),
  ],
};

Useful flags

FlagWhat it does
--mode <dev/prod/none>Set build mode
--watch, -wRebuild on file change
--config <path>Path to config file
--config-name <name>Pick named config from array
--env <key=val>Pass env to config function (module.exports = (env) => {})
--devtool <variant>Override devtool (e.g. source-map, eval-cheap-module-source-map)
--profile --json > stats.jsonEmit build stats
--progressShow progress bar
--stats <preset>Output verbosity (minimal, normal, verbose, errors-only)
--target <env>Override target (web, node, electron-main)

webpack serve flags:

FlagWhat it does
--openOpen browser on start
--port <n>Override port
--host <ip>Bind host
--hotEnable HMR (default)
--no-hotDisable HMR
--httpsSelf-signed HTTPS

Configuration

webpack's config can be a function:

javascript
module.exports = (env, argv) => ({
  mode: argv.mode || "development",
  entry: env.entry || "./src/index.js",
});
bash
npx webpack --env entry=./src/main.js --mode production

Output:

text
asset main.js 78 KiB [emitted] [minimized]
webpack 5.94.0 compiled successfully in 2104 ms

webpack.config.ts also works with ts-node or tsx registered as a loader. Most projects stick to plain webpack.config.js.

Mode defaults

ModeDefault optimizationDefault devtool
developmentNoneeval
productionMinify, tree-shake, split-chunksfalse
noneBare metalfalse

Common pitfalls

  1. Module not found: Can't resolve 'X'. Missing install, missing resolve.extensions, or Node-only import in browser code (webpack 5 dropped polyfills — add resolve.fallback or remove import).
  2. You may need an appropriate loader. A loader rule's test regex doesn't match. Add .tsx? for both TS and TSX.
  3. Slow builds. Swap babel-loader for swc-loader. Enable cache: { type: "filesystem" }. Narrow loader include paths.
  4. HMR doesn't fire. Module must call module.hot.accept(); React/Vue plugins do this. Pure side-effect modules fall back to full reload.
  5. Loaders applied in wrong order. Loaders run right-to-left in use: [...]. Reverse mentally if confused.
  6. Tree shaking didn't shrink bundle. Transitive CJS dep — CJS can't be tree-shaken. Swap for ESM alternative (lodashlodash-es).
  7. --inspect debugging webpack-dev-server. Pass via NODE_OPTIONS='--inspect-brk' npx webpack serve — DevServer wraps the build, not the user code, so debugger lands in webpack internals.

See also