Webpack Loader Execution Process: Detailed Source Code Analysis
The article details Webpack’s loader execution pipeline, showing how a shared loaderContext is built for each module, how the loader‑runner pitches loaders in order, then processes the resource and runs each loader’s normal method from right to left, handling synchronous, Promise‑based, and async callbacks via runSyncOrAsync.
This article explains the internal execution flow of Webpack loaders after a module is created.
First, a loaderContext object is created for the module, shared by all loaders. The context provides methods such as emitWarning , emitError , exec , resolve , emitFile , and properties like version , webpack , sourceMap , etc.
Key source excerpt from NormalModule.js :
const { runLoaders } = require('loader-runner')
class NormalModule extends Module {
...
createLoaderContext(resolver, options, compilation, fs) {
const loaderContext = {
version: 2,
emitWarning: warning => {...},
emitError: error => {...},
exec: (code, filename) => {...},
resolve(context, request, callback) {...},
getResolve(options) {...},
emitFile: (name, content, sourceMap) => {...},
rootContext: options.context,
webpack: true,
sourceMap: !!this.useSourceMap,
_module: this,
_compilation: compilation,
_compiler: compilation.compiler,
fs: fs
};
compilation.hooks.normalModuleLoader.call(loaderContext, this);
if (options.loader) {
Object.assign(loaderContext, options.loader);
}
return loaderContext;
}
...
}After the context is ready, runLoaders from the loader-runner package is invoked. The function initializes parameters, expands loaderContext with getters/setters for resource , request , remainingRequest , currentRequest , previousRequest , query , and data .
exports.runLoaders = function runLoaders(options, callback) {
var resource = options.resource || "";
var loaders = options.loaders || [];
var loaderContext = options.context || {};
var readResource = options.readResource || readFile;
// ...prepare loader objects, set context fields...
loaderContext.context = contextDirectory;
loaderContext.loaderIndex = 0;
loaderContext.loaders = loaders;
// define getters/setters for resource, request, etc.
// start pitching loaders
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { ... });
};The pitching phase runs each loader's pitch method in order. If a pitch returns a value, the normal phase is skipped for the remaining loaders. The core loop is:
function iteratePitchingLoaders(options, loaderContext, callback) {
if (loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
if (currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
loadLoader(currentLoaderObject, function(err) {
var fn = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
if (!fn) return iteratePitchingLoaders(options, loaderContext, callback);
runSyncOrAsync(fn, loaderContext,
[loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
function(err) { /* ... */ });
});
}Loading a loader module is handled by loadLoader.js which supports CommonJS, ES modules, and SystemJS:
module.exports = function (loader, callback) {
var module = require(loader.path);
loader.normal = module;
loader.pitch = module.pitch;
loader.raw = module.raw;
callback();
};After pitching, processResource reads the module file and starts the normal phase, where each loader's normal method is executed from right to left. Loaders can be synchronous, return a Promise, or use this.async() to obtain a callback.
function iterateNormalLoaders() {
if (loaderContext.loaderIndex < 0) return callback(null, args);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
if (currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}
var fn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;
if (!fn) return iterateNormalLoaders(options, loaderContext, args, callback);
convertArgs(args, currentLoaderObject.raw);
runSyncOrAsync(fn, loaderContext, args, function(err) {
if (err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}The helper runSyncOrAsync determines whether a loader runs synchronously, returns a Promise, or uses the async callback, and forwards results to the next loader.
function runSyncOrAsync(fn, context, args, callback) {
var isSync = true, isDone = false, reportedError = false;
context.async = function() { /* ... */ };
var innerCallback = context.callback = function() { /* ... */ };
try {
var result = fn.apply(context, args);
if (isSync) {
isDone = true;
if (result === undefined) return callback();
if (result && typeof result === "object" && typeof result.then === "function") {
return result.catch(callback).then(r => callback(null, r));
}
return callback(null, result);
}
} catch (e) { /* ... */ }
}Understanding this flow helps developers customize loader behavior, control asynchronous execution, and debug complex build pipelines.
Didi Tech
Official Didi technology account
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.