Webpack Key Concepts


  • Description: What webpack is, what problem it solves, the five core concepts (entry / output / loaders / plugins / mode), the dev server, code splitting, tree shaking, and how it compares to modern alternatives.
  • My Notion Note ID: K2A-F5-1
  • Created: 2018-03-23
  • Updated: 2026-05-17
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. The Problem Webpack Solves

Browsers historically loaded JavaScript as flat global scripts. Modern apps want:

  • Modules — split code into files that import from each other.
  • Non-JS assets — CSS, images, fonts referenced from JS modules.
  • Transpilation — TypeScript, JSX, modern ES features compiled to browser-compatible output.
  • Optimisation — minify, dead-code elimination, hash filenames for caching.

A bundler walks the import graph starting from one or more entry files, processes every reachable file (JS, CSS, images), and emits one or more bundles the browser can load. Webpack was the first bundler to make non-JS assets first-class graph nodes.

Build-tool family:

Tool Role One-liner
Make / Gulp / Grunt Task runners "Run these scripts in this order." File-level.
Webpack / Vite / esbuild / Rollup / Parcel Bundlers "Walk the import graph from this entry, emit optimised bundles." Module-level.
Babel / SWC / tsc Transpilers "Translate one source file to another." File-level, driven by the bundler.

2. Modules in JavaScript

A module is a discrete chunk of functionality with a clear public surface. Several module systems exist; webpack understands all of them.

System Syntax Where
ES Modules (ESM) import x from './a.js'; export const y = … Modern standard. Native in browsers and Node 14+.
CommonJS (CJS) const x = require('./a'); module.exports = … Node's original system, still everywhere.
AMD define(['./a'], (a) => …) RequireJS era. Legacy.
UMD Wrapper around AMD/CJS/global Library distribution, pre-ESM.

Webpack additionally treats these as module imports:

  • @import inside CSS / Sass / Less.
  • url(...) inside CSS.
  • <img src> inside HTML loaded via html-loader.

Anything in the dependency graph can be statically analysed and transformed.

3. The Five Core Concepts

entry  ──▶  loaders  ──▶  plugins  ──▶  output
              │
              v
            (mode controls defaults)

3.1 Entry

The starting point of the dependency graph. Defaults to ./src/index.js.

// webpack.config.js
module.exports = {
  entry: './src/index.js',
  // or multiple bundles
  entry: {
    main: './src/index.js',
    admin: './src/admin.js',
  },
};

Each entry becomes its own bundle. Common for multi-page apps and code splitting.

3.2 Output

Where webpack writes the bundles and how it names them. Defaults to ./dist/main.js.

const path = require('path');

module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    clean: true,                            // wipe dist/ before each build
    publicPath: '/',                        // URL prefix for assets at runtime
  },
};
Placeholder Substitutes
[name] Entry name (main, admin).
[fullhash] Hash of the entire compilation ([hash] is the deprecated alias in webpack 5).
[chunkhash] Hash per chunk.
[contenthash] Hash of the chunk's content. Use this for long-term cache busting.
[id], [query] Various. ([ext] is valid for assetModuleFilename, not for output.filename.)

3.3 Loaders

Loaders transform source files before they enter the graph. Webpack natively understands JavaScript only; loaders teach it everything else.

module.exports = {
  module: {
    rules: [
      { test: /\.tsx?$/, use: 'ts-loader' },
      { test: /\.css$/,  use: ['style-loader', 'css-loader'] },
      { test: /\.s[ac]ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
      { test: /\.(png|jpe?g|gif|svg)$/, type: 'asset/resource' },
    ],
  },
};
Rule field Purpose
test RegExp matching files this rule applies to.
use Loader(s) to apply.
exclude RegExp to skip (/node_modules/ is universal).
include Whitelist instead of blacklist.
type Built-in asset module type — see § 8.

Loaders run right-to-left (or bottom-to-top in array form). For ['style-loader', 'css-loader']:

  • css-loader first — interprets @import / url() and turns CSS into JS module.
  • style-loader second — injects the resulting CSS into a <style> tag at runtime.

Build mental model: each loader takes a string in and produces a string out (plus a source map). The output gets handed to the next loader, and finally to webpack itself.

3.4 Plugins

Plugins hook into the compilation lifecycle to do things loaders can't — emit extra files, inject globals, optimise output, generate HTML, copy static files, define environment variables.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin } = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' }),
    new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }),
    new MiniCssExtractPlugin({ filename: '[name].[contenthash].css' }),
  ],
};

Each plugin is instantiated with new. Plugins implement an apply(compiler) method and subscribe to compiler / compilation hooks.

Common plugin Purpose
HtmlWebpackPlugin Generates index.html and injects script/link tags.
MiniCssExtractPlugin Pulls CSS out of JS into separate .css files (paired with its loader instead of style-loader).
DefinePlugin (built-in) Compile-time string replacement — global constants.
CopyWebpackPlugin Copy static files into dist/ untouched.
webpack.ProvidePlugin (built-in) Auto-import a global (e.g. $ → jQuery).
CompressionPlugin Pre-compress to .gz / .br for static serving.
BundleAnalyzerPlugin Visual report of bundle composition.

3.5 Mode

module.exports = { mode: 'development' };   // or 'production' or 'none'

mode toggles preset optimisations:

Setting development production
devtool (source maps) eval (fast) false (none) — set explicitly if you want them
optimization.minimize off on (Terser)
optimization.usedExports (tree shaking) off on
process.env.NODE_ENV development production
Module IDs readable hashed

Always set mode. Without it, webpack emits a warning and runs in production defaults.

4. A Minimum Configuration

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

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    clean: true,
  },
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.(png|jpe?g|gif|svg)$/, type: 'asset/resource' },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
};
npx webpack          # one-off build
npx webpack --watch  # rebuild on file change

Webpack v4+ runs without a config file at all — defaults to ./src/index.js./dist/main.js. The config exists to override defaults.

5. Dev Server and HMR

webpack-dev-server serves the bundle from memory and reloads the browser when files change.

// webpack.config.js (additions)
module.exports = {
  // ...
  devServer: {
    static: './dist',
    port: 8080,
    open: true,            // open browser on start
    hot: true,             // Hot Module Replacement
    historyApiFallback: true,  // SPA route fallback to index.html
  },
};
npx webpack serve

HMR (Hot Module Replacement) — swap a changed module in the running page without losing state (form input, scroll position, app state). Frameworks register accept handlers (React Fast Refresh, Vue HMR API) so component edits update in place.

Caveats:

  • Not all modules HMR safely — modules with side effects (event listeners, timers) leak unless explicitly cleaned up in module.hot.dispose().
  • HMR is dev-only. Production bundles never include HMR runtime.

6. Code Splitting

Smaller bundles → faster first paint. Three flavours:

6.1 Multiple Entries

entry: {
  main:  './src/index.js',
  admin: './src/admin.js',
}

Emits main.js and admin.js. Pages load only the one they need. Risk: shared dependencies (React, lodash) duplicated across bundles — fixed by splitChunks.

6.2 splitChunks — Shared Vendor Bundle

optimization: {
  splitChunks: {
    chunks: 'all',
  },
},

Webpack analyses common dependencies and emits them as a separate vendors.js chunk. Repeat visits hit the browser cache; only the app code changes.

6.3 Dynamic Imports

button.addEventListener('click', async () => {
  const { renderChart } = await import('./chart.js');
  renderChart(data);
});

import('./chart.js') is the dynamic import syntax — webpack splits it into a separate chunk, loaded on demand. Standard ESM since ES2020.

Common for routes (React.lazy, Vue async components, Next.js automatic page splitting).

7. Tree Shaking

Eliminating exports that are imported but never used. Requires three preconditions:

  • Modules use ES module syntax (import/export, not require).
  • mode: 'production' (which enables usedExports and minifier).
  • Source code has no side effects, or package.json declares "sideEffects": false (or a list of files that do have side effects, like CSS imports).

Without sideEffects: false, webpack assumes any import might have a side effect (import './polyfills' is real) and keeps the module.

Practical tip: import only what you use. Library-specific:

// Good — only the lodash function gets bundled
import debounce from 'lodash/debounce';

// Bad — pulls in the whole library
import { debounce } from 'lodash';

// Works if lodash-es is used + sideEffects:false (lodash-es declares it)
import { debounce } from 'lodash-es';

8. Asset Modules

Built-in handling of images, fonts, etc. — no separate loader needed since webpack 5.

module: {
  rules: [
    { test: /\.png$/, type: 'asset/resource' },  // emit file, return URL
    { test: /\.svg$/, type: 'asset/inline' },     // inline as data URL
    { test: /\.txt$/, type: 'asset/source' },     // inline as raw string
    { test: /\.jpg$/, type: 'asset',              // auto-pick based on size
      parser: { dataUrlCondition: { maxSize: 8 * 1024 } } },
  ],
},

Replaces file-loader, url-loader, raw-loader from the webpack 4 era.

9. Webpack vs Modern Alternatives

Tool Pitch When to pick
Webpack Mature, most plugins/loaders, deeply customisable. Existing project on webpack; need an obscure loader; complex monorepo with webpack-specific tooling.
Vite Dev server uses native ESM (no bundle in dev → instant start); production uses Rollup. New projects. Default for Vue, used by SvelteKit, Nuxt 3, Astro.
esbuild Written in Go; 10-100× faster than JS-based bundlers. Simpler API. When you want raw speed and don't need every plugin. Used by Vite under the hood for transforms.
Rollup ESM-first, produces small clean library bundles. Publishing a library to npm.
Parcel Zero-config bundler. Quick prototypes, simple apps.
Turbopack Vercel's Rust-based bundler. Next.js default since v16 (opt out via next dev --webpack). Stable opt-in (--turbo/--turbopack) since v15. Already on Next.js. Not yet a general-purpose alternative.
Rspack Rust-based, webpack-compatible config. Migrating large webpack projects that need speed without rewriting config.

For new projects in 2026, Vite is the default starting point. Webpack remains the workhorse for large existing apps and anywhere a specific loader/plugin isn't yet ported.

10. References