Webpack Core Concepts
Webpack is not a task runner. It's a static module bundler that constructs a dependency graph starting from one or more entry points, then combines every module your project needs into one or more bundles. Understanding this mental model is the difference between cargo-culting config files and actually knowing what your build does.
The Dependency Graph
When webpack processes your application, it starts at entry points and recursively follows every import, require(), url(), and @import to discover the complete graph of dependencies:
entry.js
βββ ./components/App.jsx
β βββ react
β βββ ./Header.jsx
β β βββ ./header.module.css
β βββ ./api/client.ts
β βββ axios
βββ ./styles/global.css
β βββ ./assets/logo.png
βββ ./utils/analytics.js
βββ ./config.jsonWebpack treats everything as a module β JavaScript, CSS, images, JSON, WASM. Loaders teach webpack how to understand non-JavaScript modules; plugins extend the build process itself.
1. Entry
The entry point tells webpack where to start building the dependency graph.
Single Entry (Shorthand)
// webpack.config.js
module.exports = {
entry: './src/index.js',
};Multi-Entry (Multi-Page Applications)
module.exports = {
entry: {
home: './src/pages/home.js',
dashboard: './src/pages/dashboard.js',
admin: './src/pages/admin.js',
},
};Each entry produces a separate dependency graph and output bundle β critical for multi-page apps where you don't want dashboard code in the home page bundle.
Dynamic Entry
module.exports = {
entry: () => fetchPagesFromCMS().then(pages =>
Object.fromEntries(pages.map(p => [p.slug, p.entryFile]))
),
};Entry can be a function (sync or async) for programmatic builds. This is how some CMS-driven static site generators work under the hood.
2. Output
Output tells webpack where to emit bundles and how to name them.
const path = require('path');
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
publicPath: '/static/',
clean: true,
},
};| Property | Purpose | Example |
|---|---|---|
path | Absolute filesystem path for output | /project/dist |
filename | Bundle naming pattern | [name].[contenthash:8].js |
chunkFilename | Non-entry chunk naming | [name].[contenthash:8].chunk.js |
publicPath | URL prefix for assets in the browser | /static/, https://cdn.example.com/ |
clean | Remove old files before each build | true |
Template Strings
| Token | Meaning |
|---|---|
[name] | Entry point name or chunk name |
[contenthash] | Hash of the file's content (best for caching) |
[chunkhash] | Hash of the chunk (less granular) |
[hash] | Hash of the entire compilation (avoid β invalidates everything) |
[id] | Chunk ID |
Interview insight: Always use [contenthash] for production filenames. It enables long-term caching because a file's hash only changes when its content changes, not when unrelated modules change.
3. Mode
Mode tells webpack which built-in optimizations to enable.
module.exports = {
mode: 'production', // or 'development' | 'none'
};What Each Mode Enables
| Feature | development | production |
|---|---|---|
process.env.NODE_ENV | "development" | "production" |
| Minification (Terser) | Off | On |
| Tree shaking | Off | On |
| Module concatenation | Off | On |
| Source maps | eval (fast) | None (you set manually) |
| Named modules | Yes (readable IDs) | No (numeric IDs) |
DefinePlugin | Sets NODE_ENV | Sets NODE_ENV |
optimization.minimize | false | true |
optimization.concatenateModules | false | true |
Under the hood, mode: 'production' is equivalent to:
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
concatenateModules: true, // "scope hoisting"
splitChunks: { chunks: 'all' },
usedExports: true, // enables tree shaking
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
};4. Loaders
Webpack only understands JavaScript and JSON natively. Loaders transform other file types into valid modules that webpack can process.
module.exports = {
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset', // webpack 5 built-in asset modules
},
],
},
};Loader Chaining (Right to Left)
Loaders execute right to left (or bottom to top in the array). Each loader receives the output of the previous one:
style-loader β css-loader β postcss-loader β sass-loader
β β β β
Injects <style> Resolves Applies Compiles
tag into DOM @import/url() autoprefixer .scss β .css{
test: /\.scss$/,
use: [
'style-loader', // 4. Injects CSS into DOM via <style>
'css-loader', // 3. Resolves @import and url()
'postcss-loader', // 2. Runs autoprefixer, etc.
'sass-loader', // 1. Compiles SCSS to CSS β starts here
],
}The Pitch Phase
Before normal execution (right to left), loaders run a pitch phase (left to right). If a loader's pitch function returns a value, the chain short-circuits:
Pitch phase: style-loader.pitch β css-loader.pitch β sass-loader.pitch
Normal phase: sass-loader β css-loader β style-loaderThis is how style-loader works β its pitch function intercepts the chain to inline CSS at runtime. Understanding pitch is rare knowledge that signals deep webpack expertise.
5. Plugins
Plugins hook into webpack's entire compilation lifecycle. While loaders transform individual files, plugins operate on the compilation as a whole.
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const webpack = require('webpack');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
minify: { collapseWhitespace: true, removeComments: true },
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
}),
new webpack.DefinePlugin({
__APP_VERSION__: JSON.stringify('1.2.3'),
'process.env.API_URL': JSON.stringify('https://api.example.com'),
}),
],
};Key Plugins to Know
| Plugin | Purpose |
|---|---|
HtmlWebpackPlugin | Generates HTML file with script/link tags injected |
MiniCssExtractPlugin | Extracts CSS into separate files (replaces style-loader in production) |
DefinePlugin | Compile-time constant substitution (dead code elimination) |
CopyWebpackPlugin | Copies static files to output directory |
BundleAnalyzerPlugin | Visualizes bundle composition and sizes |
ProvidePlugin | Auto-imports modules when a variable is used (e.g., $ β jQuery) |
EnvironmentPlugin | Shorthand DefinePlugin for process.env.* |
Tapable: The Plugin System
Webpack's plugin system is built on Tapable, which provides different hook types:
class MyPlugin {
apply(compiler) {
// Synchronous hook
compiler.hooks.compile.tap('MyPlugin', (params) => {
console.log('Compilation starting...');
});
// Async hook (promise-based)
compiler.hooks.emit.tapPromise('MyPlugin', async (compilation) => {
const assets = Object.keys(compilation.assets);
console.log(`Emitting ${assets.length} assets`);
});
}
}Module Resolution
Webpack uses enhanced-resolve to find modules. Understanding resolution rules saves hours of debugging:
module.exports = {
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
alias: {
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
modules: ['src', 'node_modules'],
mainFields: ['browser', 'module', 'main'],
},
};Resolution Algorithm
When webpack encounters import Button from '@components/Button':
- Check if it's a path (
./,../,/) or a module name - For aliases: replace
@componentsβ/abs/path/src/components - Append each extension:
Button.tsx,Button.ts,Button.jsx,Button.js - If directory, look for
indexfile with each extension - For bare specifiers (e.g.,
react): walk upnode_modulesdirectories, then checkresolve.modules - Read
package.jsonand check fields inmainFieldsorder
import 'lodash/debounce'
β ./node_modules/lodash/debounce.js (resolve.extensions)
β ./node_modules/lodash/debounce/index.js (directory index)
β ../node_modules/lodash/debounce.js (walk up)Mental Model: The Build Pipeline
Source Files Webpack Pipeline Output
ββββββββββββ βββββββββββββββββββββββββββββββ ββββββββββββββββ
β .js .ts β β 1. Resolve entry points β β main.a1b2.js β
β .jsx .tsxβββββΆβ 2. Build dependency graph βββββΆβ vendor.c3d4.jsβ
β .css β β 3. Apply loaders (transform)β β style.e5f6.cssβ
β .scss β β 4. Bundle modules β β index.html β
β .png .svgβ β 5. Run plugins (optimize) β β images/ β
β .json β β 6. Emit assets β β fonts/ β
ββββββββββββ βββββββββββββββββββββββββββββββ ββββββββββββββββSteps 2β3 happen per-module. Steps 4β6 happen on the full compilation. Loaders fire during step 3; most plugins fire during steps 4β6 via Tapable hooks on the Compiler and Compilation objects.
Interview Power Moves
- "Webpack builds a dependency graph, not a file list." This shows you understand that webpack statically analyzes import relationships rather than globbing files.
- "Loaders are per-file transforms; plugins are compilation-wide." The clearest way to articulate the difference.
- "
contenthashoverchunkhashbecause it's content-addressed." Shows you understand cache invalidation granularity. - "The pitch phase lets loaders short-circuit the chain." Most candidates have never heard of pitch loaders β this signals deep knowledge.
- "Mode is syntactic sugar over a set of optimization defaults." Demonstrates that you've read beyond the docs summary.