Demystifying Webpack: A Deep Dive into Its Build Process and a Simple Implementation

This article walks through Webpack’s complete build pipeline—from initialization and configuration parsing, through module loading, compilation, and chunk generation, to asset emission—while also providing a concise reference implementation of a minimal Webpack clone for hands‑on learning.

ELab Team
ELab Team
ELab Team
Demystifying Webpack: A Deep Dive into Its Build Process and a Simple Implementation

Webpack Build Process Overview

Recently I skimmed the Webpack source code and gained a basic understanding of its bundling workflow. In this article I share the overall build process, explain key concepts, and provide a simple reference implementation of a minimal Webpack.

Webpack is a static module bundler for modern JavaScript applications. It combines every module required by your project into one or more bundles for use in the browser.

Overall Build Flow

Webpack runs as a serial process, executing the following steps from start to finish:

Key Concepts

Module

In modular programming, a module is a discrete chunk of functionality. In Webpack, a module corresponds to each resource file (JS, CSS, images, etc.) instantiated by NormalModule and stored in compilation.modules. It represents an intermediate state between a source file and a chunk.

Chunk & ChunkGroup

A chunk is an output file (entry, async chunk, or split chunk) that can contain multiple modules. Chunks are stored in compilation.chunks. A ChunkGroup groups related chunks; the entry point itself is a ChunkGroup containing the entry chunk.

Loader

Webpack can only process JavaScript modules natively. Loaders are Node modules exported as functions that transform other file types (e.g., CoffeeScript, JSX, LESS, images). The loader receives the source file and returns the transformed result, allowing require to load any type of file.

module.exports = function (source) {
    // ... process source
    return newSource;
};

Plugins

Plugins are pluggable modules that can perform tasks beyond what loaders can do. They rely on the Tapable library, which implements a publish‑subscribe hook system. Plugins tap into hooks (sync, async, parallel, etc.) and execute callbacks when those hooks are called.

const tapable = require("tapable");
const { SyncHook } = tapable;
const hook = new SyncHook();

hook.tap("MyHook", () => { console.log("enter MyHook"); });
hook.tap("MyHook2", () => { console.log("enter MyHook2"); });

hook.call(); // enter MyHook   enter MyHook2
class MyPlugin {
  apply(compiler) {
    compiler.hooks.make.tap("MyPlugin", (compilation) => {
      // ...
      compilation.hooks.optimizeChunkAssets.tap("MyPlugin", (chunks) => {
        // ...
      });
    });
  }
}

Plugins must define an apply method on their prototype and receive a compiler instance.

The apply method is called during plugin initialization, allowing subscription to compiler and compilation hooks.

Webpack publishes specific events at various points in the compilation cycle; plugins react to these events and can use Webpack’s API to modify the build result.

Compiler

The Compiler object holds the current Webpack configuration (entry, output, loaders, etc.). It is instantiated once when Webpack starts and is globally unique. Plugins can access configuration via this object and subscribe to its hooks.

class Compiler extends Tapable {
  constructor(context) {
    this.options = {};
    this.hooks = {
      run: new AsyncSeriesHook(["compiler"]),
      compile: new SyncHook(["params"]),
      make: new AsyncParallelHook(["compilation"]),
      // ... other hooks
    };
  }
}

Compilation

A Compilation represents a single build version. Each time a build runs (e.g., in watch mode), a new Compilation is created. It tracks modules, generated assets, file changes, and dependency graphs. The compilation lifecycle includes loading, sealing, optimizing, and asset emission.

class Compilation {
  addEntry(context, entry, name, callback) {
    this.hooks.addEntry.call(entry, name);
    this._addModuleChain(context, entry, (module) => this.entries.push(module), (err, module) => {
      this.hooks.succeedEntry.call(entry, name, module);
      return callback(null, module);
    });
  }
  // ... other methods
}

Detailed Build Steps

Initialization

Running ./node_modules/.bin/webpack executes node_modules/webpack/bin/webpack.js, which loads the CLI (webpack‑cli). The CLI parses arguments, merges webpack.config.js with command‑line options, creates a global compiler instance, and calls compiler.run() for non‑watch builds.

// bin/webpack.js
const installedClis = CLIs.filter(cli => cli.installed);
if (installedClis.length === 0) {
  // prompt user to install a CLI
} else if (installedClis.length === 1) {
  const path = require("path");
  const pkgPath = require.resolve(`${installedClis[0].package}/package.json`);
  const pkg = require(pkgPath);
  require(path.resolve(path.dirname(pkgPath), pkg.bin[installedClis[0].binName]));
} else {
  // warning for multiple CLIs
}

The CLI then merges options, creates the compiler via require("webpack")(options), and starts the compilation.

Compilation

Calling compiler.run() triggers a series of hooks ( beforeRun, run, beforeCompile, compile, make, etc.). The make hook starts the real compilation: loaders parse modules, dependencies are resolved recursively, and modules are built.

run() {
  const onCompiled = (err, compilation) => {
    this.emitAssets(compilation, err => {
      this.emitRecords(err => {
        this.hooks.done.callAsync(stats, err => {
          return finalCallback(null, stats);
        });
      });
    });
  };

  this.hooks.beforeRun.callAsync(this, err => {
    this.hooks.run.callAsync(this, err => {
      this.readRecords(err => {
        this.compile(onCompiled);
      });
    });
  });
}

compile(callback) {
  const params = this.newCompilationParams();
  this.hooks.beforeCompile.callAsync(params, err => {
    this.hooks.compile.call(params);
    const compilation = this.newCompilation(params);
    this.hooks.make.callAsync(compilation, err => {
      compilation.finish(err => {
        compilation.seal(err => {
          this.hooks.afterCompile.callAsync(compilation, err => {
            return callback(null, compilation);
          });
        });
      });
    });
  });
}

During make, plugins such as SingleEntryPlugin add entry modules, and the module factory creates NormalModule instances that run loaders and the parser.

apply(compiler) {
  compiler.hooks.make.tapAsync(
    "SingleEntryPlugin",
    (compilation, callback) => {
      const { entry, name, context } = this;
      const dep = SingleEntryPlugin.createDependency(entry, name);
      compilation.addEntry(context, dep, name, callback);
    }
  );
}

The loader runner executes each loader in sequence, transforms the source, and passes the result to the parser, which generates an AST and extracts dependencies.

// loader-runner
runLoaders(options, callback) {
  // set up loaderContext
  // execute loaders (including pitch phase)
  // finally call callback with transformed source
}
// parser example
parse(source, initialState) {
  let ast;
  if (typeof source === "object" && source !== null) {
    ast = source;
  } else {
    ast = Parser.parse(source, { sourceType: this.sourceType, onComment: comments });
  }
  // hook into program, detect mode, walk statements to collect dependencies
}

Seal and Output

After all modules are built, the seal phase creates chunks, assigns IDs, runs optimization plugins, and generates assets. Finally, Compiler.emitAssets writes each asset to the output file system.

seal(callback) {
  this.hooks.seal.call();
  this.hooks.beforeChunks.call();
  // create entry chunks, connect modules and chunks
  // build chunk graph, sort modules, apply IDs, create hashes
  // generate chunk assets
  this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
    this.applyModuleIds();
    this.applyChunkIds();
    this.createHash();
    this.createModuleAssets();
    if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
      this.hooks.beforeChunkAssets.call();
      this.createChunkAssets();
    }
    this.summarizeDependencies();
    callback();
  });
}

emitAssets(compilation, callback) {
  const emitFiles = () => {
    asyncLib.forEachLimit(compilation.getAssets(), 15, ({ name: file, source }, cb) => {
      const targetPath = path.join(this.outputPath, file);
      this.outputFileSystem.writeFile(targetPath, source.source(), cb);
    });
  };
  this.outputFileSystem.mkdirp(this.outputPath, emitFiles);
}

Webpack Build Process Summary

Initialize options by merging configuration files and CLI arguments.

Instantiate a global Compiler, load plugins, and start the run phase.

Determine entry points from the entry configuration.

Recursively compile modules: loaders transform files, the parser builds an AST, and dependencies are resolved.

After all modules are compiled, create chunks based on the dependency graph and generate output assets.

Write the generated assets to the file system according to the configured output path and filenames.

How to Implement a Simple Webpack

Provide a configuration file similar to webpack.config.js (e.g., mywebpack.config.js).

Implement a Compiler that reads options, creates a Compilation, and runs the build.

Implement a Compilation that runs loaders, parses files into an AST, and generates chunks.

Support a Tapable‑based plugin system.

Support configurable loaders for file transformation.

Follow the overall flow init → run → make → seal → emit.

Reference implementation: https://github.com/tangxiaojun1996/mywebpack

References

Webpack 5 documentation: https://juejin.cn/post/6882663278712094727

Tapable library: https://github.com/webpack/tapable

AST Explorer: https://astexplorer.net/

Various blog posts and articles listed in the original source.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

webpackPluginsBuild Processmodule bundlerloaders
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.