Module Bundler

Webpack

Webpack is the battle-tested module bundler. It turns a graph of modules, assets and dependencies into optimised bundles — and understanding it makes every other build tool clearer.

advanced14 min readUpdated Sep 15, 2026
webpack.config.js
js
// webpack.config.js
import path from "node:path";

export default {
  entry: "./src/index.js",
  output: {
    path: path.resolve(import.meta.dirname, "dist"),
    filename: "[name].[contenthash].js",
    clean: true,
  },
  module: {
    rules: [
      { test: /\.css$/, use: ["style-loader", "css-loader"] },
    ],
  },
  optimization: { splitChunks: { chunks: "all" } },
};
First release
2012
Model
Dependency graph
Transforms
Loaders
Extends
Plugins
Dev server
webpack-dev-server
Still used by
Many enterprise apps

Why it matters

Why Webpack still matters

Handles everything

JavaScript, CSS, images, fonts and more all flow through one pipeline, so the graph is complete and explicit.

Deeply configurable

Loaders, plugins and optimisation options let you shape the build precisely when the defaults do not fit.

Powerful optimisation

Code splitting, tree shaking, caching and minification are all available, with fine-grained control.

The big picture

The three ideas behind Webpack

A dependency graph, loaders that transform files, and plugins that extend the build.

The graph

Discover

Starting from entry points, Webpack follows imports to build a graph of every module and asset.

Loaders

Transform

Functions that turn a file into a module, such as compiling TypeScript or processing CSS.

Plugins

Extend

Hooks into the build lifecycle for tasks beyond transforming individual files.

Webpack at a glance

The core of Webpack

Entry

The starting modules from which Webpack builds the dependency graph.

Output

Where bundles are written and how they are named, including content hashes.

Loaders

Transform files before they enter the graph, such as babel-loader or css-loader.

Plugins

Extend the build with HTML generation, environment variables and more.

Code splitting

Dynamic import and splitChunks create separate bundles loaded on demand.

Tree shaking

Remove unused exports, which requires ES modules and side-effect-free code.

A short history

The bundler that built the modern web

  1. 2012

    Webpack released

    Tobias Koppers introduces a bundler that treats every asset as a module.

    12
  2. 2014

    Webpack 1

    The bundler becomes popular with the rise of React and single-page apps.

    14
  3. 2016

    Webpack 2

    Native ES modules and tree shaking arrive.

    16
  4. 2020

    Webpack 5

    Persistent caching, module federation and asset modules ship.

    20
  5. Today

    Still in production

    Vite is the modern default, but Webpack remains widespread in existing apps.

    Today

The complete guide

Webpack: Everything you need to know

What is Webpack?

Webpack is a module bundler. Starting from one or more entry points, it follows every import and builds a graph of your application’s modules and assets. It then transforms and combines them into a small number of bundles the browser can load efficiently.

Released in 2012, Webpack became the backbone of the modern front-end build. Vite is now the default for new projects because it is faster and simpler, but Webpack still powers an enormous number of production applications, and its concepts — the dependency graph, loaders, plugins and code splitting — are the vocabulary of every bundler that followed.

Entry and output

The two required pieces of configuration are where the graph starts and where the bundles go.

// webpack.config.js
import path from "node:path";

export default {
  entry: "./src/index.js",
  output: {
    path: path.resolve(import.meta.dirname, "dist"),
    filename: "[name].[contenthash].js",
    clean: true,
  },
};

entry is the starting module. output.filename uses a content hash so browsers can cache bundles forever and only re-download changed ones. clean: true removes stale files before each build.

Loaders

Webpack only understands JavaScript by default. Loaders transform other files into modules as they enter the graph.

// loaders.js
export default {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: "ts-loader",
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.(png|svg|woff2)$/,
        type: "asset",
      },
    ],
  },
};

A rule has a test for which files it matches and a use for the loader or loaders to apply. Loaders run right to left, so ["style-loader", "css-loader"] first parses the CSS and then injects it into the page. Webpack 5 also has asset modules, which replace the old file-loader and url-loader for images and fonts.

Plugins

Plugins extend the build itself. Where a loader transforms a file, a plugin hooks into the compilation lifecycle.

// plugins.js
import HtmlWebpackPlugin from "html-webpack-plugin";
import { DefinePlugin } from "webpack";

export default {
  plugins: [
    new HtmlWebpackPlugin({ template: "./src/index.html" }),
    new DefinePlugin({
      "process.env.NODE_ENV": JSON.stringify("production"),
    }),
  ],
};

Common plugins generate the HTML file, define environment variables, copy static assets, analyse bundle size and clean the output. This is where most of Webpack’s power — and most of its complexity — lives.

Resolve and aliases

The resolve options control how imports are found.

// resolve.js
export default {
  resolve: {
    extensions: [".ts", ".tsx", ".js"],
    alias: {
      "@": path.resolve(import.meta.dirname, "src"),
    },
  },
};

extensions lets you import without specifying a file extension, and alias creates shortcuts so imports are not littered with ../../.. paths. The same alias is usually mirrored in the TypeScript config so the editor agrees with the bundler.

Code splitting and tree shaking

Two optimisations dominate production performance.

Code splitting breaks the bundle into chunks loaded on demand. Dynamic import() is the primary tool, and splitChunks automatically extracts shared dependencies.

// split.js
export default {
  optimization: {
    splitChunks: { chunks: "all" },
  },
};

Tree shaking removes unused exports, but only when it can analyse the imports statically. That means ES modules (import/export) rather than CommonJS, and modules that are free of side effects. Marking packages as sideEffects: false in their package.json lets Webpack prune aggressively.

The development server

webpack-dev-server serves the build in development with hot module replacement.

// devServer.js
export default {
  devServer: {
    port: 3000,
    hot: true,
    historyApiFallback: true,
    proxy: [{ context: ["/api"], target: "http://localhost:8787" }],
  },
};

historyApiFallback makes client-side routing work by serving index.html for unknown paths, and proxy forwards API requests to your backend. The dev server keeps the build in memory, so rebuilds are fast.

When to use Webpack

Webpack is still a strong choice when you maintain an existing application, depend on a specific loader or plugin, or need advanced features such as module federation for micro-frontends. Its configuration is verbose, but it is also extremely capable and predictable once understood.

For new projects, Vite is usually the better default: faster startup, simpler configuration and a modern plugin API. Learning Webpack is still worthwhile because its concepts transfer directly — Vite’s plugin interface is Rollup-compatible and the same ideas of graph, transforms and splitting apply everywhere.

Best practices

  • Keep configuration split into common, dev and prod files or functions.
  • Use content hashes in filenames for long-term caching.
  • Prefer ES modules so tree shaking can work.
  • Split heavy features with dynamic imports.
  • Mark side-effect-free packages so unused code is removed.
  • Mirror resolve.alias in the TypeScript config.
  • Analyse the bundle with a visualiser when it grows unexpectedly.

Common mistakes

  • Shipping everything in one bundle and never code splitting.
  • Using CommonJS and wondering why tree shaking does nothing.
  • Putting a loader in the wrong order and getting confusing errors.
  • Duplicating configuration across environments instead of composing it.
  • Aliasing in Webpack but not in TypeScript, so the editor disagrees.
  • Adding a plugin that duplicates a built-in Webpack 5 feature.

Where to go next

Webpack is the historical foundation of modern build tooling and still a dependable choice for existing applications. Compare it with Vite for new projects, understand the npm packages that supply loaders and plugins, and see how the same ideas appear in Turborepo for monorepo builds. Then read your project’s Webpack config and trace one file from entry to output.

Enabling tree shaking

Tree shaking needs ES module syntax. CommonJS require calls are dynamic enough that unused exports cannot be removed safely.

Prefer
// math.js
export function add(a, b) {
  return a + b;
}
export function unused() {}

// only add is bundled
Avoid
// math.js
module.exports = {
  add: (a, b) => a + b,
  unused: () => {},
};
// both shipped to the browser

Splitting code

Dynamic import creates a separate chunk loaded only when needed. Static imports put everything in the initial bundle.

Prefer
async function openEditor() {
  const { Editor } = await import(
    "./Editor"
  );
  return Editor;
}
Avoid
import { Editor } from "./Editor";
// heavy editor in the
// initial bundle for everyone

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Webpack?

Our interactive tutorial walks you through Webpack step by step — with quizzes and real code you can run in the browser.