Under The Hood

This section describes webpack internals and can be useful for plugin developers

The bundling is a function that takes some files and emits others.

But between input and output, it also has modules, entry points, chunks, chunk groups, and many other intermediate parts.

The main parts

Every file used in your project is a Module

./index.js

import app from "./app.js";

./app.js

export default "the app";

By using each other, the modules form a graph (ModuleGraph).

During the bundling process, modules are combined into chunks. Chunks combine into chunk groups and form a graph (ChunkGraph) interconnected through modules. When you describe an entry point - under the hood, you create a chunk group with one chunk.

./webpack.config.js

export default {
  entry: "./index.js",
};

One chunk group with the main name created (main is the default name for an entry point). This chunk group contains ./index.js module. As the parser handles imports inside ./index.js new modules are added into this chunk.

Another example:

./webpack.config.js

export default {
  entry: {
    home: "./home.js",
    about: "./about.js",
  },
};

Two chunk groups with names home and about are created. Each of them has a chunk with a module - ./home.js for home and ./about.js for about

There might be more than one chunk in a chunk group. For example SplitChunksPlugin splits a chunk into one or more chunks.

Chunks

Chunks come in two forms:

  • initial is the main chunk for the entry point. This chunk contains all the modules and their dependencies that you specify for an entry point.
  • non-initial is a chunk that may be lazy-loaded. It may appear when dynamic import or SplitChunksPlugin is being used.

Each chunk has a corresponding asset. The assets are the output files - the result of bundling.

webpack.config.js

export default {
  entry: "./src/index.jsx",
};

./src/index.jsx

import { createRoot } from "react-dom/client";

import("./app.jsx").then((App) => {
  const root = createRoot(document.getElementById("root"));
  root.render(<App />);
});

Initial chunk with name main is created. It contains:

  • ./src/index.jsx
  • react
  • react-dom

and all their dependencies, except ./app.jsx

Non-initial chunk for ./app.jsx is created as this module is imported dynamically.

Output:

  • /dist/main.js - an initial chunk
  • /dist/394.js - non-initial chunk

By default, there is no name for non-initial chunks so that a unique ID is used instead of a name. When using dynamic import we may specify a chunk name explicitly by using a "magic" comment:

import(
  /* webpackChunkName: "app" */
  "./app.jsx"
).then((App) => {
  const root = createRoot(document.getElementById("root"));
  root.render(<App />);
});

Output:

  • /dist/main.js - an initial chunk
  • /dist/app.js - non-initial chunk

Output

The names of the output files are affected by the two fields in the config:

  • output.filename - for initial chunk files
  • output.chunkFilename - for non-initial chunk files
  • In some cases chunks are used initial and non-initial. In those cases output.filename is used.

A few placeholders are available in these fields. Most often:

  • [id] - chunk id (e.g. [id].js -> 485.js)
  • [name] - chunk name (e.g. [name].js -> app.js). If a chunk has no name, then its id will be used
  • [contenthash] - md4-hash of the output file content (e.g. [contenthash].js -> 4ea6ff1de66c537eb9b2.js)

The build lifecycle

Those parts are produced in a fixed order, and every build runs the same phases. Plugins hook into them: compiler hooks mark the phases themselves, compilation hooks cover everything inside make and seal.

compiler created
  environment -> afterEnvironment -> entryOption -> afterPlugins
  -> afterResolvers -> initialize
  |
run (or watch)
  beforeRun -> run
  |
compile                        one Compilation per build
  beforeCompile -> compile -> thisCompilation -> compilation
  |
make                           files become modules
  addEntry -> resolve -> buildModule -> succeedModule
  ...repeated for every dependency found...
  finishMake -> finishModules
  |
seal                           modules become chunks, chunks become assets
  seal -> optimizeDependencies
  -> beforeChunks -> afterChunks
  -> optimize -> optimizeModules -> optimizeChunks -> optimizeTree
  -> moduleIds -> chunkIds
  -> codeGeneration -> runtimeRequirements -> beforeHash -> afterHash
  -> processAssets -> afterProcessAssets -> afterSeal
  |
emit                           assets become files
  afterCompile -> shouldEmit -> emit -> assetEmitted -> afterEmit
  |
done
  done -> afterDone

Two phases carry most of the work.

make builds the module graph. Each entry is handed to a module factory, which resolves the request to a file. The module is then built — its loaders run over the source, and the result is parsed for dependencies. Every dependency found is resolved and built the same way, so the graph grows outwards until nothing new is reached. finishModules is the first point at which the whole graph exists.

seal turns that graph into output. Modules are assigned to chunks, the chunk graph is optimized, ids and hashes are assigned, and code is generated — and only then do assets exist. That ordering is why asset work belongs in processAssets rather than in the compiler's emit hook, which runs after the compilation is already sealed.

ToTap
see or change how a request resolvesthe normalModuleFactory hooks
read a module once it is builtsucceedModule
act once the graph is completefinishModules
change how modules land in chunksoptimizeChunks
add, edit or remove an assetprocessAssets, at the matching stage
read the finished builddone
Edit this page·
« Previous
Why webpack

4 Contributors

smelukovEugeneHlushkochenxsanamirsaeed671