🐣HatchlingWebpackBuild ToolsBundling

Webpack Core Concepts

Webpack builds a dependency graph from your source code and bundles it into optimized outputs. Mastering the five core concepts β€” entry, output, mode, loaders, and plugins β€” is the foundation for everything else.

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.json

Webpack 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,
  },
};
PropertyPurposeExample
pathAbsolute filesystem path for output/project/dist
filenameBundle naming pattern[name].[contenthash:8].js
chunkFilenameNon-entry chunk naming[name].[contenthash:8].chunk.js
publicPathURL prefix for assets in the browser/static/, https://cdn.example.com/
cleanRemove old files before each buildtrue

Template Strings

TokenMeaning
[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

Featuredevelopmentproduction
process.env.NODE_ENV"development""production"
Minification (Terser)OffOn
Tree shakingOffOn
Module concatenationOffOn
Source mapseval (fast)None (you set manually)
Named modulesYes (readable IDs)No (numeric IDs)
DefinePluginSets NODE_ENVSets NODE_ENV
optimization.minimizefalsetrue
optimization.concatenateModulesfalsetrue

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-loader

This 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

PluginPurpose
HtmlWebpackPluginGenerates HTML file with script/link tags injected
MiniCssExtractPluginExtracts CSS into separate files (replaces style-loader in production)
DefinePluginCompile-time constant substitution (dead code elimination)
CopyWebpackPluginCopies static files to output directory
BundleAnalyzerPluginVisualizes bundle composition and sizes
ProvidePluginAuto-imports modules when a variable is used (e.g., $ β†’ jQuery)
EnvironmentPluginShorthand 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':

  1. Check if it's a path (./, ../, /) or a module name
  2. For aliases: replace @components β†’ /abs/path/src/components
  3. Append each extension: Button.tsx, Button.ts, Button.jsx, Button.js
  4. If directory, look for index file with each extension
  5. For bare specifiers (e.g., react): walk up node_modules directories, then check resolve.modules
  6. Read package.json and check fields in mainFields order
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.
  • "contenthash over chunkhash because 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.