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
- 2. Modules in JavaScript
- 3. The Five Core Concepts
- 4. A Minimum Configuration
- 5. Dev Server and HMR
- 6. Code Splitting
- 7. Tree Shaking
- 8. Asset Modules
- 9. Webpack vs Modern Alternatives
- 10. References
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:
@importinside CSS / Sass / Less.url(...)inside CSS.<img src>inside HTML loaded viahtml-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-loaderfirst — interprets@import/url()and turns CSS into JS module.style-loadersecond — 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, notrequire). mode: 'production'(which enablesusedExportsand minifier).- Source code has no side effects, or
package.jsondeclares"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
- Webpack documentation — https://webpack.js.org/concepts/
- Webpack Modules — https://webpack.js.org/concepts/modules/
- Webpack Plugin API — https://webpack.js.org/api/plugins/
- Tree shaking guide — https://webpack.js.org/guides/tree-shaking/
- Vite — https://vite.dev/
- esbuild — https://esbuild.github.io/
- Turbopack — https://turbo.build/pack