Toolmingo
Guides13 min read

Webpack Cheat Sheet: Config, Loaders, Plugins, and Optimization

A complete Webpack 5 cheat sheet — webpack.config.js setup, loaders, plugins, code splitting, dev server, module federation, and common patterns. Copy-ready examples.

Webpack is the most widely used JavaScript module bundler. This reference covers everything from the basic webpack.config.js to advanced code splitting and optimisation in Webpack 5.

Quick reference

The 25 Webpack patterns you use every day.

Pattern What it does
npx webpack Bundle with default config
mode: 'development' Fast build, readable output
mode: 'production' Minified, optimised output
entry: './src/index.js' Single entry point
entry: { app, vendor } Multiple entry points
output.filename: '[name].[contenthash].js' Cache-busting filenames
output.clean: true Clear dist/ before each build
module.rules Array of loader rules
test: /\.css$/ Match files by extension regex
use: ['style-loader', 'css-loader'] Chain loaders (right to left)
plugins: [new HtmlWebpackPlugin()] Add plugins
devServer: { hot: true } Enable HMR
devtool: 'source-map' Full source maps
devtool: 'eval-cheap-module-source-map' Fast dev source maps
optimization.splitChunks Automatic code splitting
import() Dynamic import (lazy chunk)
resolve.alias Path aliases
resolve.extensions Auto-resolve extensions
externals Exclude from bundle (CDN libs)
DefinePlugin Replace constants at build time
MiniCssExtractPlugin Extract CSS to separate files
webpack-bundle-analyzer Visualise bundle size
module federation Share modules between apps
cache: { type: 'filesystem' } Persistent build cache
experiments.outputModule Output ESM bundles

Installation

# Install webpack and CLI
npm install --save-dev webpack webpack-cli

# Install dev server for local development
npm install --save-dev webpack-dev-server

# Common loaders and plugins
npm install --save-dev \
  babel-loader @babel/core @babel/preset-env @babel/preset-react \
  css-loader style-loader \
  html-webpack-plugin \
  mini-css-extract-plugin \
  css-minimizer-webpack-plugin

Add scripts to package.json:

{
  "scripts": {
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development",
    "start": "webpack serve --mode development",
    "analyze": "webpack --mode production --env analyze"
  }
}

Basic config

webpack.config.js — the minimal config that covers most projects:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // Build mode: 'development' | 'production' | 'none'
  mode: 'development',

  // Entry point(s)
  entry: './src/index.js',

  // Output
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    clean: true, // Clean dist/ before build (Webpack 5+)
    publicPath: '/',
  },

  // Loaders
  module: {
    rules: [
      // JavaScript + JSX via Babel
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      // CSS
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      // Images and fonts
      {
        test: /\.(png|jpg|gif|svg|woff2?|ttf|eot)$/,
        type: 'asset/resource', // Webpack 5 built-in (replaces file-loader)
      },
    ],
  },

  // Plugins
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],

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

  // Source maps
  devtool: 'eval-cheap-module-source-map',

  // Dev server
  devServer: {
    port: 3000,
    hot: true,
    historyApiFallback: true, // SPA fallback for React Router
    open: true,
  },
};

Entry and output

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

// Multiple entries (creates separate bundles)
entry: {
  app: './src/app.js',
  admin: './src/admin.js',
}

// Entry with dependencies
entry: {
  app: {
    import: './src/app.js',
    dependOn: 'shared',
  },
  admin: {
    import: './src/admin.js',
    dependOn: 'shared',
  },
  shared: ['react', 'react-dom'],
}

// Output
output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].[contenthash:8].js',   // Cache busting
  chunkFilename: '[name].[contenthash:8].chunk.js',
  assetModuleFilename: 'assets/[hash][ext]',
  clean: true,
  publicPath: 'auto', // Webpack 5: infer from document.currentScript
}

Loaders

Loaders transform files before they are bundled. They run right to left in a use array.

JavaScript: Babel

npm install --save-dev babel-loader @babel/core @babel/preset-env
// webpack.config.js
{
  test: /\.(js|jsx|ts|tsx)$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: [
        ['@babel/preset-env', { targets: 'defaults' }],
        '@babel/preset-react',
        '@babel/preset-typescript',
      ],
    },
  },
}
// babel.config.json (preferred over options inline)
{
  "presets": [
    ["@babel/preset-env", { "targets": "defaults" }],
    "@babel/preset-react",
    "@babel/preset-typescript"
  ]
}

TypeScript: ts-loader

npm install --save-dev ts-loader typescript
{
  test: /\.tsx?$/,
  use: 'ts-loader',
  exclude: /node_modules/,
}

Or use Babel with @babel/preset-typescript (no type-checking, but faster):

{
  test: /\.tsx?$/,
  use: 'babel-loader', // with @babel/preset-typescript in babel config
  exclude: /node_modules/,
}

CSS

npm install --save-dev css-loader style-loader postcss-loader
npm install --save-dev mini-css-extract-plugin  # Extract to file
// Development: inject into DOM via <style> tag
{
  test: /\.css$/,
  use: ['style-loader', 'css-loader'],
}

// Production: extract to .css file
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
{
  test: /\.css$/,
  use: [MiniCssExtractPlugin.loader, 'css-loader'],
}

// With PostCSS (Tailwind, autoprefixer)
{
  test: /\.css$/,
  use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: ['tailwindcss', 'autoprefixer'],
        },
      },
    },
  ],
}

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

Sass/SCSS

npm install --save-dev sass sass-loader
{
  test: /\.s[ac]ss$/,
  use: ['style-loader', 'css-loader', 'sass-loader'],
}

Static assets (Webpack 5)

Webpack 5 replaces file-loader and url-loader with built-in Asset Modules:

// Emit a file and export the URL
{ test: /\.(png|jpg|gif)$/, type: 'asset/resource' }

// Export as data URI (base64)
{ test: /\.svg$/, type: 'asset/inline' }

// Auto: small files → inline, large files → resource
{
  test: /\.(png|jpg)$/,
  type: 'asset',
  parser: {
    dataUrlCondition: { maxSize: 8 * 1024 }, // 8 KB threshold
  },
}

// Raw file content as string
{ test: /\.txt$/, type: 'asset/source' }

Plugins

HtmlWebpackPlugin

npm install --save-dev html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');

new HtmlWebpackPlugin({
  template: './public/index.html',  // Your HTML template
  title: 'My App',                  // Inject into <title>
  favicon: './public/favicon.ico',
  minify: {
    collapseWhitespace: true,
    removeComments: true,
  },
})

MiniCssExtractPlugin

npm install --save-dev mini-css-extract-plugin css-minimizer-webpack-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

plugins: [
  new MiniCssExtractPlugin({
    filename: '[name].[contenthash:8].css',
    chunkFilename: '[name].[contenthash:8].chunk.css',
  }),
],

optimization: {
  minimizer: [
    '...', // Keep default JS minimizer (TerserPlugin)
    new CssMinimizerPlugin(),
  ],
},

DefinePlugin

Replace constants at compile time:

const { DefinePlugin } = require('webpack');

new DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
  'process.env.API_URL': JSON.stringify(process.env.API_URL),
  __DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
  __VERSION__: JSON.stringify(require('./package.json').version),
})

CopyWebpackPlugin

npm install --save-dev copy-webpack-plugin
const CopyPlugin = require('copy-webpack-plugin');

new CopyPlugin({
  patterns: [
    { from: 'public/robots.txt', to: 'robots.txt' },
    { from: 'public/manifest.json' },
  ],
})

Mode and environments

Use a function to return different configs per environment:

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = (env, argv) => {
  const isDev = argv.mode === 'development';

  return {
    mode: argv.mode,
    devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map',

    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
            'css-loader',
          ],
        },
      ],
    },

    plugins: [
      new HtmlWebpackPlugin({ template: './public/index.html' }),
      ...(!isDev ? [new MiniCssExtractPlugin()] : []),
    ],
  };
};

Or merge separate configs:

npm install --save-dev webpack-merge
// webpack.common.js
const common = { entry: './src/index.js', /* ... */ };

// webpack.dev.js
const { merge } = require('webpack-merge');
module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: { hot: true, port: 3000 },
});

// webpack.prod.js
module.exports = merge(common, {
  mode: 'production',
  devtool: 'source-map',
});
// package.json
{
  "scripts": {
    "start": "webpack serve --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  }
}

Dev server

devServer: {
  port: 3000,
  host: '0.0.0.0',     // Expose to network
  hot: true,           // Hot Module Replacement
  open: true,          // Open browser on start
  compress: true,      // gzip responses
  historyApiFallback: true, // SPA: serve index.html for all routes

  // Proxy API calls to backend
  proxy: [
    {
      context: ['/api'],
      target: 'http://localhost:8080',
      changeOrigin: true,
      pathRewrite: { '^/api': '' },
    },
  ],

  // Serve static files
  static: {
    directory: path.resolve(__dirname, 'public'),
  },

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

Source maps

devtool value Build speed Rebuild speed Quality Use case
false Fastest Fastest None Production (no maps)
eval Fastest Fastest Low Very fast builds
eval-cheap-module-source-map Fast Fast Medium Dev recommended
cheap-module-source-map Medium Fast Medium CI builds
source-map Slow Slow Best Production recommended
hidden-source-map Slow Slow Best Production + Sentry
nosources-source-map Slow Slow No source Production (hide source)

Code splitting

Dynamic imports (lazy chunks)

// React lazy loading
import React, { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings  = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Router>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Router>
    </Suspense>
  );
}
// Named chunks for better debugging
const module = await import(/* webpackChunkName: "analytics" */ './analytics');

SplitChunksPlugin (automatic vendor splitting)

optimization: {
  splitChunks: {
    chunks: 'all', // Split both sync and async chunks

    cacheGroups: {
      // Put all node_modules in one vendor chunk
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        priority: 10,
        reuseExistingChunk: true,
      },
      // Separate React from other vendors
      react: {
        test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
        name: 'react',
        priority: 20,
      },
      // Bundle shared code used by 2+ chunks
      common: {
        name: 'common',
        minChunks: 2,
        priority: 5,
        reuseExistingChunk: true,
      },
    },
  },

  // Extract webpack runtime to its own chunk
  runtimeChunk: 'single',
}

Prefetch and preload

// Prefetch (low priority, loaded during idle)
import(/* webpackPrefetch: true */ './HeavyModal');

// Preload (high priority, fetched in parallel with parent)
import(/* webpackPreload: true */ './CriticalChart');

Environment variables

# .env file (use dotenv-webpack)
npm install --save-dev dotenv-webpack
const Dotenv = require('dotenv-webpack');

plugins: [
  new Dotenv({
    path: `./.env.${argv.mode}`, // .env.development or .env.production
    safe: true,                  // Require .env.example
    systemvars: true,            // Also load from process.env
  }),
]
# .env.development
API_URL=http://localhost:8080
FEATURE_FLAGS=debug,verbose

# .env.production
API_URL=https://api.example.com
// Access in code
const url = process.env.API_URL;

TypeScript

npm install --save-dev typescript ts-loader
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"]
}
// webpack.config.js (using ts-loader)
{
  test: /\.tsx?$/,
  use: [
    {
      loader: 'ts-loader',
      options: {
        transpileOnly: true, // Skip type-checking (use tsc separately for CI)
      },
    },
  ],
  exclude: /node_modules/,
}

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

React setup

npm install react react-dom
npm install --save-dev @babel/preset-react
// babel.config.json
{
  "presets": [
    ["@babel/preset-env", { "targets": "defaults" }],
    ["@babel/preset-react", { "runtime": "automatic" }]
  ]
}
// webpack.config.js (minimal React setup)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.jsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    clean: true,
  },
  module: {
    rules: [
      { test: /\.(js|jsx)$/, exclude: /node_modules/, use: 'babel-loader' },
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
    ],
  },
  resolve: { extensions: ['.js', '.jsx'] },
  plugins: [new HtmlWebpackPlugin({ template: './public/index.html' })],
  devServer: { port: 3000, hot: true, historyApiFallback: true },
};

Bundle analysis

npm install --save-dev webpack-bundle-analyzer
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = (env) => ({
  plugins: [
    ...(env.analyze ? [new BundleAnalyzerPlugin()] : []),
  ],
});
npm run build -- --env analyze
# Opens interactive treemap in browser

Module federation (Webpack 5)

Share code between separately deployed apps (micro-frontends):

// Shell app (host)
const { ModuleFederationPlugin } = require('webpack').container;

new ModuleFederationPlugin({
  name: 'shell',
  remotes: {
    checkout: 'checkout@http://localhost:3001/remoteEntry.js',
    catalog: 'catalog@http://localhost:3002/remoteEntry.js',
  },
  shared: {
    react: { singleton: true, eager: true },
    'react-dom': { singleton: true, eager: true },
  },
})

// Use remote component
const CheckoutWidget = lazy(() => import('checkout/CheckoutWidget'));
// Checkout app (remote)
new ModuleFederationPlugin({
  name: 'checkout',
  filename: 'remoteEntry.js',
  exposes: {
    './CheckoutWidget': './src/components/CheckoutWidget',
  },
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})

Performance optimisation

// Production config optimisations
module.exports = {
  mode: 'production',

  optimization: {
    // Tree shaking (automatic in production)
    usedExports: true,
    sideEffects: true, // Honour package.json "sideEffects" field

    // Minification
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: { drop_console: true },
        },
      }),
      new CssMinimizerPlugin(),
    ],

    // Code splitting
    splitChunks: { chunks: 'all' },
    runtimeChunk: 'single',
  },

  // Persistent filesystem cache (Webpack 5 — huge speed boost)
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename],
    },
  },

  // Only rebuild changed modules
  snapshot: {
    managedPaths: [path.resolve(__dirname, 'node_modules')],
  },
}

Exclude large libraries (use CDN)

// webpack.config.js
externals: {
  react: 'React',
  'react-dom': 'ReactDOM',
  lodash: '_',
}
<!-- public/index.html -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>

Common patterns

Alias @/ to src/

resolve: {
  alias: {
    '@': path.resolve(__dirname, 'src'),
    '@components': path.resolve(__dirname, 'src/components'),
    '@utils': path.resolve(__dirname, 'src/utils'),
  },
}
// Usage in code
import Button from '@/components/Button';
import { formatDate } from '@utils/date';

Handle SVG as React components

npm install --save-dev @svgr/webpack
{
  test: /\.svg$/,
  use: [
    {
      loader: '@svgr/webpack',
      options: { svgoConfig: { plugins: [{ name: 'removeViewBox', active: false }] } },
    },
  ],
}
import { ReactComponent as Logo } from './logo.svg';
// or (Webpack 5 named export)
import Logo from './logo.svg?react';

Multi-page application (MPA)

const pages = ['index', 'about', 'contact'];

module.exports = {
  entry: Object.fromEntries(pages.map((p) => [p, `./src/pages/${p}.js`])),
  plugins: pages.map(
    (p) =>
      new HtmlWebpackPlugin({
        filename: `${p}.html`,
        template: `./public/${p}.html`,
        chunks: [p],
      }),
  ),
};

Progress output

const { ProgressPlugin } = require('webpack');
plugins: [new ProgressPlugin()]

Common mistakes

Mistake Problem Fix
Using file-loader / url-loader in Webpack 5 Deprecated, warnings Use type: 'asset/resource' / 'asset/inline'
Loaders in wrong order CSS not applied use: ['style-loader', 'css-loader'] (right→left)
output.publicPath mismatch Assets 404 in production Set publicPath: '/' or 'auto'
No contenthash in filename Browser caches stale bundles Use [contenthash] in filenames
Committing dist/ to git Large repo, merge conflicts Add dist/ to .gitignore
No exclude: /node_modules/ on babel-loader Extremely slow builds Always exclude node_modules
DefinePlugin without JSON.stringify Runtime errors JSON.stringify('value') not 'value'
Forgetting runtimeChunk: 'single' Long-term cache broken Add to optimization in production

Webpack vs other bundlers

Feature Webpack 5 Vite Parcel esbuild
Config complexity High Low Zero Low
Dev server speed Moderate Very fast (native ESM) Fast Very fast
Prod build speed Slow Fast (Rollup) Moderate Fastest
Ecosystem maturity Most plugins Growing Mature Limited
Code splitting Excellent Good Good Basic
Module federation Yes (native) Via plugin No No
Legacy browser support Best Via plugin Good Limited
Config language JS / TS TS first Auto-detect Go / JS API
Best for Complex enterprise apps Modern SPAs Zero-config Libraries

CLI flags

Command What it does
webpack Build once
webpack serve Start dev server
webpack --watch Watch mode (rebuild on change)
webpack --config webpack.prod.js Custom config file
webpack --mode production Override mode
webpack --profile --json > stats.json Generate build stats
webpack --analyze (needs env variable or plugin)
webpack --env key=value Pass env variable to config function
webpack --progress Show build progress
webpack --display-error-details Verbose error output

FAQ

Q: Should I use Webpack or Vite in 2025?

For new projects, Vite is generally the better choice: faster dev server via native ES modules, simpler config, and comparable production output. Use Webpack when you need advanced features (Module Federation, complex multi-page setups, legacy browser support) or are maintaining an existing Webpack project.

Q: What's the difference between webpack and webpack-cli?

webpack is the core bundler; webpack-cli provides the command-line interface. You need both to run npx webpack from the terminal. For programmatic use, you only need webpack.

Q: How do I speed up slow Webpack builds?

  1. Add cache: { type: 'filesystem' } (Webpack 5 — biggest win).
  2. Use ts-loader with transpileOnly: true (skip type-checking during build).
  3. Exclude node_modules from babel-loader.
  4. Use thread-loader for CPU-intensive loaders.
  5. Use eval-cheap-module-source-map in dev (not source-map).

Q: Why does process.env.NODE_ENV not work in my frontend code?

Webpack doesn't automatically inject process.env. You must use DefinePlugin:

new DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
})

Vite automatically replaces import.meta.env.MODE — Webpack requires the explicit plugin setup.

Q: What's tree shaking and does Webpack do it automatically?

Tree shaking removes unused exports from your bundle. Webpack does it automatically in mode: 'production' for ES modules. For it to work: 1) use ES module syntax (import/export, not require/module.exports), 2) add "sideEffects": false (or a list of side-effectful files) in package.json of your libraries.

Q: How do I handle environment-specific API URLs?

Use dotenv-webpack with .env.development and .env.production files, or use DefinePlugin with process.env.API_URL set via the build command:

API_URL=https://api.example.com npm run build

Then in webpack.config.js:

new DefinePlugin({ 'process.env.API_URL': JSON.stringify(process.env.API_URL) })

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools