How Tapable Powers Webpack: Inside the Hook System
This article explains the relationship between Tapable and webpack, detailing how Tapable implements a flexible hook system that powers webpack's plugin architecture, covering core concepts, hook classifications, usage patterns, internal mechanics, and practical examples for building custom webpack plugins.
Tapable and webpack relationship
Tapable is a library similar to Node.js EventEmitter, focusing on custom event triggering and handling. Webpack uses Tapable to decouple implementation from process, with all concrete implementations existing as plugins.
What is webpack?
Webpack is a static module bundler for modern JavaScript applications. It builds a dependency graph that maps each module required by the project and generates one or more bundles.
Important webpack modules
entry
output
loader (transforms source code of modules)
plugin (injects extension logic at specific points in the build process)
Plugins are the backbone of webpack; webpack itself is built on the same plugin system used in its configuration.
Webpack build process
Webpack essentially works as an event flow mechanism, chaining plugins together. The core of this mechanism is Tapable. The Compiler (responsible for compilation) and Compilation (responsible for creating bundles) are instances of Tapable (pre‑webpack 5). After webpack 5 they are defined via a hooks property. Tapable implements a complex publish‑subscribe pattern.
Example with Compiler:
// webpack5 before, using inheritance
...
const { Tapable, SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require("tapable");
...
class Compiler extends Tapable {
constructor(context) {
super();
...
}
}
// webpack5
...
const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require("tapable");
...
class Compiler {
constructor(context) {
this.hooks = Object.freeze({
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
...
})
}
...
}How to use Tapable
Tapable exposes nine Hook classes. Instantiating a Hook creates an execution flow and provides registration and execution methods. Different Hook types lead to different execution flows.
Classification by sync/async
Sync – can only be registered with synchronous functions, e.g., myHook.tap() AsyncSeries – can be registered with synchronous, callback‑based, or promise‑based functions; execution is serial.
AsyncParallel – same registration options; execution is parallel.
Classification by execution mode
Basic – executes every registered function, ignoring return values.
Bail – stops after the first function returns a non‑undefined result.
Waterfall – passes the result of the previous function as the first argument to the next.
Loop – repeatedly executes functions until all return undefined.
Hook usage pattern
Instantiate a Hook class.
Register one or more callbacks.
Call the hook with arguments.
(Optional) Add interceptors to listen to registration or execution.
Simple SyncHook example:
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
hook.tap("1", (arg1, arg2, arg3) => {
console.log(1, arg1, arg2, arg3);
return 1;
});
hook.tap("2", (arg1, arg2, arg3) => {
console.log(2, arg1, arg2, arg3);
return 2;
});
hook.tap("3", (arg1, arg2, arg3) => {
console.log(3, arg1, arg2, arg3);
return 3;
});
hook.call("a", "b", "c");
// Output:
// 1 a b c
// 2 a b c
// 3 a b cAsynchronous AsyncSeriesHook example:
let { AsyncSeriesHook } = require("tapable");
let queue = new AsyncSeriesHook(["name"]);
console.time("cost");
queue.tapPromise("1", function (name) {
return new Promise(resolve => {
setTimeout(() => {
console.log(1, name);
resolve();
}, 1000);
});
});
queue.tapPromise("2", function (name) {
return new Promise(resolve => {
setTimeout(() => {
console.log(2, name);
resolve();
}, 2000);
});
});
queue.tapPromise("3", function (name) {
return new Promise(resolve => {
setTimeout(() => {
console.log(3, name);
resolve();
}, 3000);
});
});
queue.promise("weiyi").then(data => {
console.log(data);
console.timeEnd("cost");
});HookMap usage
A HookMap helps manage a map of hooks. Official recommendation is to instantiate all hooks on a class property hooks:
<code class="language-js">class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}
setSpeed(newSpeed) {
this.hooks.accelerate.call(newSpeed);
}
}
const myCar = new Car();
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
myCar.setSpeed(1);
</code>MultiHook usage
MultiHook redirects taps to multiple other hooks:
this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);Tapable internals
The core of Tapable is the Hook class, which stores registration methods ( tap, tapAsync, tapPromise) and execution methods ( call, callAsync, promise). The HookCodeFactory creates the actual function body that runs the registered callbacks, handling sync, async series, async parallel, and loop execution strategies.
Key steps in the execution flow:
Collect taps into this.taps (optionally sorted by stage or before).
When the hook is invoked, _createCall builds a compile function via HookCodeFactory.
The compile function is generated with new Function(...), assembling a header and a body that iterates over taps according to the hook type.
For sync hooks the generated code simply calls each tap in order:
var _fn0 = _x[0];
_fn0(arg1, arg2, arg3);
var _fn1 = _x[1];
_fn1(arg1, arg2, arg3);
...Async series hooks wrap each tap in a _next function that calls the next tap after the previous one invokes its callback. Promise hooks create a promise chain, checking that each tap returns a promise and propagating results or errors.
How Tapable empowers webpack plugins
Plugins register callbacks to webpack’s hooks (exposed via compiler.hooks or compilation.hooks). When webpack reaches a specific step, it triggers the corresponding hook, executing all registered plugin logic.
Example plugin that logs when the build starts:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, compilation => {
console.log('webpack build started!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;In the Compiler class the run hook is an AsyncSeriesHook. During the build process webpack calls this.hooks.run.callAsync(this, …), which invokes the plugin’s callback.
Summary
Tapable exposes nine hook types; core methods are registration, execution, and interceptors.
Tapable implements hooks by concatenating strings based on hook and registration types, then feeding them to the Function constructor.
Webpack uses Tapable to manage its entire build pipeline, allowing flexible customization of each step.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
