Bundlers
A summary of build-related content
Updated
Bundle
Purpose
- Compress code, reduce size
- Merge files, reduce the number of HTTP requests
- Syntax conversion, converting non-js syntax into js syntax
Main Process
- Collect dependencies
- Generate the dependency graph
- Build and assemble the code
Implementation
// A module bundler compiles small pieces of code into larger and more complex code that can run in a Web browser.
// These small pieces are just JavaScript files and the dependencies between them, which are exactly what the module system represents
// https://webpack.js.org/concepts/modules
// A module bundler has the concept of an entry file. Instead of adding some
// script tags in the browser and letting them run, we let the bundler know
// which file is the main file of our application. This file can bootstrap
// our entire application.
// Our bundler will start from that entry file and try to understand
// which files it depends on. Then, it will try to understand which files
// its dependencies depend on, and it keeps doing this until it discovers
// every module in our application and how they depend on each other.
// This understanding of the project is called the `dependency graph`.
// In this example, we will create a dependency graph and use it to bundle
// all of its modules together.
// Let's begin :)
// > Please note: this is a very simplified example.
// Only one pass of handling cyclic dependencies, caching module exports, and parsing each module
// is performed; other aspects are skipped to keep this example as simple as possible.
const fs = require('node:fs')
const path = require('node:path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')
let ID = 0
// We first create a function that accepts a file path, reads the content, and extracts its dependencies.
function createAsset(filename) {
// Read the file's content as a string.
const content = fs.readFileSync(filename, 'utf-8')
// Now we try to find out which files this file depends on. We can do that by looking at its content
// for `import` strings. However, this is a very clunky approach, so we will use a JavaScript parser instead.
// A JavaScript parser is a tool that can read and understand JavaScript code.
// They generate a more abstract model called an `ast (abstract syntax tree)`.
// I strongly recommend you check out [`ast explorer`](https://astexplorer.net) to see what an `ast` looks like
// The `ast` contains a lot of information about our code. We can query it to understand what our code is trying to do.
const ast = babylon.parse(content, {
sourceType: 'module',
})
// This array will hold the relative paths of the modules this module depends on.
const dependencies = []
// We traverse the `ast` to try to understand which modules this module depends on.
// To do that, we check every `import` declaration in the `ast`. ❤️
traverse(ast, {
// `Ecmascript` modules are fairly simple because they are static. This means you can't `import` a variable,
// or conditionally `import` another module.
// Every time we see an `import` declaration, we can count its value as a `dependency`.
ImportDeclaration: ({ node }) => {
// We push the value we import into the dependencies array. ⬅️
dependencies.push(node.source.value)
},
})
// We also assign a unique identifier to this module by incrementing a simple counter.
const id = ID++
// We use `Ecmascript` modules and other JavaScript features that may not be supported in all browsers.
// To ensure `our bundle` runs in all browsers,
// we will use [babel](https://babeljs.io) to transpile it
// The `presets` option is a set of rules that tells `babel` how to transpile our code.
// We use `babel-preset-env` to convert our code into something browsers can run.
const { code } = transformFromAst(ast, null, {
presets: ['env'],
})
// Return all the information about this module.
return {
id,
filename,
dependencies,
code,
}
}
// Now that we can extract the dependencies of a single module, we will solve the problem by extracting the dependencies of the `entry file`.
// Then, we will extract the dependencies of each of its dependencies. And keep looping
// until we understand every module in the application and how they depend on each other. This understanding of the project is called the `dependency graph`.
function createGraph(entry) {
// First parse the entire file.
const mainAsset = createAsset(entry)
// We will use a `queue` to parse the dependencies of each `asset`.
// We are defining an array with only the entry asset.
const queue = [mainAsset]
// We use a `for ... of` loop to iterate over the queue.
// Initially this queue only has one asset, but as we iterate it, we will push additional new assets into the queue.
// This loop will terminate when the queue is empty.
for (const asset of queue) {
// Each of our assets has a list of relative paths of the modules it depends on.
// We will iterate over them, parse them with our `createAsset()` function, and track this module's dependencies in this object.
asset.mapping = {}
// This is the directory this module is in.
const dirname = path.dirname(asset.filename)
// We iterate over its list of relative paths
asset.dependencies.forEach((relativePath) => {
// Our `createAsset()` function needs an absolute filename.
// But the dependencies array holds an array of relative paths.
// These paths are relative to the file importing them.
// We can turn the relative path into an absolute one by joining the relative path with the path of the parent asset's directory.
const absolutePath = path.join(dirname, relativePath)
// Parse the asset, read its content, and extract its dependencies.
const child = createAsset(absolutePath)
// It is important for us to know that `asset` depends on `child`.
// We express this one-to-one relationship by adding a new property (with value child.id) to the `asset.mapping` object.
asset.mapping[relativePath] = child.id
// Finally, we push the `child` asset into the queue so its dependencies will also be iterated and parsed.
queue.push(child)
})
}
// At this step, the queue is an array containing every module in the target application:
// this is our representation graph.
return queue
}
// Next, we define a function that will use our `graph` and return a bundle that can run in the browser.
// Our bundle will have just one self-invoking function:
// `(function() {})()`
// The function will receive only one parameter: an object containing the information of each module in the `graph`.
function bundle(graph) {
let modules = ''
// Before we get to the body of that function, we will build the object that serves as its parameter.
// Please note that the string we build is wrapped by two curly braces ({}), so for each module,
// we add a string of this format: `key: value,`.
graph.forEach((mod) => {
// Each module in the graph has an `entry` in this object. We use the `module id` as the `key` and an array as the `value` (an array because we have 2 values in each module).
// The first value is each module's code wrapped in a function. This is because modules should be scoped: defining a variable in one module should not affect other modules or the global scope.
// After our modules are `transpiled {by babel}`, they use the `commonjs` module system: they expect a `require`, a `module`, and an `exports` object to be available. Those are typically not available in browsers, so we will implement them and inject them into the function wrapper.
// For the second value, we use `stringify` to serialize the relationship between the module and its dependencies (i.e., the asset.mapping above). The serialized object looks like this: `{'./relative/path': 1}`.
// This is because our transpiled modules call `require()` with relative paths. When this function is called, we should be able to know which module in the dependency graph corresponds to that relative path for this module.
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`
})
// Finally, we implement the body of the self-invoking function.
// We first create a `require()`⏰ function: it accepts a `module ID` and looks it up in the `modules` object we built earlier.
// Get our wrapper function and the `mappings` object by destructuring `const [fn, mapping] = modules[id]`.
// Our module's code calls `require()` with relative paths instead of module IDs.
// But our `require`🌟 function receives a `module ID`. Also, two modules may `require()` the same relative path but mean two different modules.
// To handle this, when a module is required, we create a new, dedicated `require` function for it to use.
// It will be specific and will know to convert `its relative paths` into `IDs` by using `the module's mapping object`.
// This mapping object happens to be the mapping between `relative paths and module IDs` for that specific module.
// Lastly, with `commonjs`, when a module needs to be exported, it can expose the module's values by changing the exports object.
// The require function will finally return the exports object.
const result = `
(function(modules) {
function require(id) { //🌟
const [fn, mapping] = modules[id];
function localRequire(name) { //⏰
return require(mapping[name]); //🌟
}
const module = { exports: {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`
// We simply return the result, hooray! :)
return result
}
const graph = createGraph('./example/entry.js')
const result = bundle(graph)
console.log(result)
tree-shaking
Modules That Support tree-shaking
- Generally only ESM modules are supported; CJS modules need extra tooling to handle, and the effect is not as good as with ESM modules
- Use the form of individual imports: both
import * as Aandimport { A }work, butimport Adoes not package.jsondoes not definesideEffects, or its value is nottrue
Babel 7 converts code to cjs by default, causing esm modules to fail tree-shaking properly
Hot Update
---
title: Hot Update Principle
---
graph TB
Editor[Text Editor/IDE]
File[File System]
subgraph DevServer[Webpack Dev Server]
Compiler[Webpack Compiler]
HMR[HMR Server]
Bundle[Bundle Server]
end
subgraph Browser[Browser]
subgraph BundleJS[bundle.js]
HMRRuntime[HMR Runtime]
JS[Built JS Code]
end
end
Editor -->|Save changes| File
File -->|Trigger build| Compiler
Compiler -->|Notify the hot update server which resources changed| HMR
Compiler -->|Transmit the built Chunk| Bundle
Bundle -->|Make the local chunk accessible to the browser via WS| BundleJS
HMRRuntime -->|Output the locally built chunk to the browser| JS
File Fingerprints
JS File Fingerprints
hash: unique to the entire project; updates after each rebuildchunkhash: generates thehashaccording to theentry; when one of thechunks built from theentryis updated, the value changescontenthash: independent for eachchunk; as long as the currentchunk's content has not changed, it will not change
file-loader File Fingerprints
hash: unique to the entire project; updates after each rebuildcontenthash: independent for eachchunk; as long as the currentchunk's content has not changed, it will not change
Webpack Build Output Analysis
Overall Structure
Webpack wraps the transformed module output in a closure function, in the following format:
// import, require, etc. are replaced with the __webpack_require__ function
// export, exports are unified into adding fields to exports
function(__unused_webpack_module, exports, __webpack_require__) {
// In production mode, the eval here is replaced with the actual file content instead of executing via eval
eval('Transformed JS code of the file')
}
The modules in the project are recorded in __webpack_modules__; if already executed, the exports are cached in the __webpack_module_cache__ object
/** The key is the module ID, generally the file path relative to the root directory, or an npm package name */
var __webpack_modules__ = {
// The entry file
// In production mode, the key is the file's sequence number — the order in which it was accessed — so the entry file is generally 0
"./src/entry.js": ((__unused_webpack_module, exports, __webpack_require__) => {
// In production mode, the eval here is replaced with the actual file content instead of executing via eval
eval("Execute the entry file code")
})
}
The __webpack_require__ function is responsible for loading modules; its code is as follows:
function __webpack_require__(moduleId) {
// If the module has already been executed, don't execute it again; directly return the cached module exports
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports
}
// If the module has not been executed, initialize the module cache first, with exports defaulting to an empty object
var module = __webpack_module_cache__[moduleId] = {
exports: {}
}
// Execute the module, also collecting the module's exports
__webpack_modules__[moduleId](module, module.exports, __webpack_require__)
// Return the content exported by the module
return module.exports
}
Synchronous Chunks
Synchronous chunks are inlined directly into __webpack_modules__, e.g.:
/** The key is the module ID, generally the file path relative to the root directory, or an npm package name */
var __webpack_modules__ = {
// Modules are arranged in order of reference, with the entry at the bottom
// Some variables are appropriately renamed to avoid variable name conflicts
// In production mode, the key is the file's sequence number — the order in which it was accessed
"./src/a.js": ((__unused_webpack_module, exports, __webpack_require__) => {
// In production mode, the eval here is replaced with the actual file content instead of executing via eval
eval("Transformed a.js code")
}),
// In production mode, the key is the file's sequence number — the order in which it was accessed
"./src/b.js": ((__unused_webpack_module, exports, __webpack_require__) => {
// In production mode, the eval here is replaced with the actual file content instead of executing via eval
eval("Transformed b.js code")
}),
// In production mode, the key is the file's sequence number — the order in which it was accessed
"./src/c.mjs": ((__unused_webpack_module, exports, __webpack_require__) => {
// ESM enables strict mode
"use strict";
// In production mode, the eval here is replaced with the actual file content instead of executing via eval
eval("Transformed c.mjs code")
}),
// Other modules
// ...
// The entry file
// In production mode, the key is the file's sequence number — the order in which it was accessed — so the entry file is generally 0
"./src/entry.js": ((__unused_webpack_module, exports, __webpack_require__) => {
// In production mode, the eval here is replaced with the actual file content instead of executing via eval
eval("Execute the entry file code")
})
}
Asynchronous Chunks
Async chunks are not inlined directly into __webpack_modules__, but loaded dynamically via JSONP. Webpack generates a separate file for each async chunk and loads it via a script tag when needed.
// The loading mechanism for async chunks
// 1. Define the global chunk loading function
var chunkLoadingGlobal = self["webpackChunk"] = self["webpackChunk"] || [];
// 2. Define the JSONP callback function to handle loaded chunks
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [chunkIds, moreModules, runtime] = data
// Add the new modules to __webpack_modules__
for (var moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId]
}
}
// Execute the runtime code
if (runtime) {
var result = runtime(__webpack_require__)
}
// Mark the chunks as loaded
for (var i = 0; i < chunkIds.length; i++) {
var chunkId = chunkIds[i]
if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
installedChunks[chunkId][0]()
}
installedChunks[chunkId] = 0
}
return __webpack_require__.O(result)
}
// 3. Override the chunkLoadingGlobal.push method so it can handle newly loaded chunks
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal))
When dynamic import syntax such as import() or require.ensure() is used, webpack generates code similar to the following:
// Dynamic import in the entry file
__webpack_require__.e(/* chunkId */ 889).then(__webpack_require__.bind(__webpack_require__, 889)).then((module) => {
// Use the asynchronously loaded module
console.log(module.default)
});
// Async chunk file (chunk-889.js)
(self["webpackChunk"] = self["webpackChunk"] || []).push([[889], {
// Module definition
"./src/async-module.js": (__unused_webpack_module, exports, __webpack_require__) => {
"use strict"
eval("Async module code")
}
}])
Main characteristics of async chunks:
- On-demand loading: only loaded when needed, reducing initial loading time
- Independent files: each async chunk generates an independent JS file
- JSONP loading: dynamically loaded via JSONP, avoiding blocking the main thread
- Caching mechanism: loaded chunks are cached to avoid repeated loading
Webpack Runtime Functions
Webpack injects some runtime functions during the bundling process to support module loading and execution. Here are two important runtime functions:
The __webpack_require__.o Function
__webpack_require__.o is a helper function used to check whether an object owns a specific property; it is a wrapper around Object.prototype.hasOwnProperty.call:
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})()
This function is widely used inside webpack, mainly for:
- Checking whether a module has been loaded
- Checking whether an object contains a specific property
- Determining whether a property exists during module merging and dependency resolution
For example, when loading an async chunk, this function is used to check whether the module already exists in __webpack_modules__:
for (var moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId]
}
}
The __webpack_require__.e Function
__webpack_require__.e is the core function for loading async chunks; it is responsible for creating script tags and handling the loading process:
/* webpack/runtime/ensure chunk */
(() => {
__webpack_require__.f = {}
// This object stores the chunks that have already been loaded
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises)
return promises
}, []))
}
})()
When dynamic imports (such as import()) are used, webpack generates code similar to the following:
// Dynamic import in the entry file
__webpack_require__.e(/* chunkId */ 889).then(__webpack_require__.bind(__webpack_require__, 889)).then((module) => {
// Use the asynchronously loaded module
console.log(module.default)
})
The main functions of __webpack_require__.e:
- Check whether the chunk has already been loaded
- If not loaded, create a script tag to load the corresponding chunk file
- Return a Promise that resolves when the chunk finishes loading
- Handle loading failures, providing an error handling mechanism
These two functions are important components of the webpack runtime environment; together they support webpack's module loading mechanism, especially the on-demand loading of async chunks.
The __webpack_require__.f Function
__webpack_require__.f is an object containing the different strategies webpack uses to load async chunks. In webpack 5, it mainly includes the following loading methods:
/* webpack/runtime/ensure chunk */
(() => {
__webpack_require__.f = {}
// 1. Loading via script tag
__webpack_require__.f.j = (chunkId, promises) => {
// Check whether the chunk is already loaded
if (!__webpack_require__.o(installedChunks, chunkId) || installedChunks[chunkId] === undefined) {
// Create a Promise and store it in installedChunks
var promise = new Promise((resolve, reject) => {
installedChunks[chunkId] = [resolve, reject]
})
promises.push(promise)
// Create a script tag to load the chunk
var script = document.createElement('script')
script.src = __webpack_require__.p + __webpack_require__.u(chunkId)
script.onerror = () => {
reject(new Error('Chunk load failed for chunk: ' + chunkId))
}
document.head.appendChild(script)
} else {
promises.push(installedChunks[chunkId])
}
}
// 2. Loading via import() (ESM)
__webpack_require__.f.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] })
}
}
}
// 3. Loading via require.ensure (CommonJS)
__webpack_require__.f.require = (chunkId, promises) => {
// Similar to script tag loading, but using the require.ensure syntax
}
})()
The main roles of the __webpack_require__.f object:
- Provide multiple loading strategies: support script tags, import(), require.ensure, and other loading methods
- Unified loading interface: uniformly invoke these loading strategies through the
__webpack_require__.efunction - Handle loading states: track the loading state of each chunk, ensuring each is loaded only once
- Error handling: provide an error handling mechanism for loading failures
In webpack 5, the __webpack_require__.f.j strategy is used by default, i.e., loading async chunks via script tags. The advantages of this approach are:
- Good compatibility, supporting all browsers
- Multiple chunks can be loaded in parallel
- Supports caching and preloading
- Can handle loading failures
__webpack_require__.f, together with __webpack_require__.e and __webpack_require__.o, forms the core mechanism of webpack's asynchronous loading, enabling webpack to support code splitting and on-demand loading.
The __webpack_require__.O Function
__webpack_require__.O is a helper function in the webpack runtime used to handle callbacks after asynchronous loading completes:
/* webpack/runtime/ensure chunk */
(() => {
__webpack_require__.O = (result, fn) => (fn ? fn() : result)
})()
The main roles of this function are:
- Simplify the processing logic after asynchronous loading: provide a concise way to handle the result when an async chunk finishes loading
- Support optional callback functions: if a callback function is provided, execute it; otherwise directly return the result
- Uniformly handle asynchronous loading results: used in webpack's asynchronous loading mechanism to uniformly handle the logic after loading completes
In webpack's asynchronous loading code, you usually see usage like this:
// After async loading completes, process the result via __webpack_require__.O
__webpack_require__.e(/* chunkId */ 889)
.then(__webpack_require__.bind(__webpack_require__, 889))
.then((module) => {
// Use the asynchronously loaded module
console.log(module.default)
})
.then(__webpack_require__.O) // Use __webpack_require__.O to process the result
The design of the __webpack_require__.O function reflects the simplicity and flexibility of webpack runtime functions: through a simple function wrapper, it implements the processing of asynchronous loading results, making webpack's asynchronous loading mechanism more complete and robust.
The Async Task Queue
In the webpack build output, besides the runtime functions above, there is another important mechanism for maintaining async tasks: the async task queue (queue). This queue is mainly used to handle asynchronously loaded modules and resources.
/* webpack/runtime/ensure chunk */
(() => {
// Initialize the async task queue
var installedChunks = {
// The chunkId of the main bundle (entry file) is 0
0: 0
}
// The async task queue, used to store asynchronously loaded modules
var deferredModules = []
// The async loading function
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises)
return promises
}, []))
}
// The function that processes the async task queue
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [chunkIds, moreModules, runtime] = data
// Add the new modules to __webpack_modules__
for (var moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId]
}
}
// Execute the runtime code
if (runtime) {
var result = runtime(__webpack_require__)
}
// Mark the chunks as loaded
for (var i = 0; i < chunkIds.length; i++) {
var chunkId = chunkIds[i]
if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
installedChunks[chunkId][0]()
}
installedChunks[chunkId] = 0
}
// Process the deferred modules
if (deferredModules.length) {
deferredModules.forEach(([moduleId, chunkId]) => {
if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId] === 0) {
__webpack_require__(moduleId)
}
})
deferredModules = []
}
return __webpack_require__.O(result)
}
// Override the chunkLoadingGlobal.push method
var chunkLoadingGlobal = self.webpackChunk = self.webpackChunk || []
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal))
})()
The main features and roles of the async task queue:
Deferred module management: the
deferredModulesarray is used to store information about modules that need deferred loading, including module IDs and corresponding chunk IDs.Async loading state tracking: the
installedChunksobject is used to track the loading state of each chunk, ensuring each chunk is loaded only once.Async loading completion handling: when an async chunk finishes loading, the
webpackJsonpCallbackfunction processes the loading result and adds the deferred modules to__webpack_modules__.Deferred module execution: after an async chunk finishes loading, it checks whether there are modules in
deferredModulesthat need to be executed, and executes them if so.Preventing duplicate loading: tracks loaded chunks via the
installedChunksobject, avoiding loading the same chunk repeatedly.
This async task queue mechanism enables webpack to efficiently handle asynchronously loaded modules, achieve on-demand loading and code splitting, and ensure the correct loading order and dependencies of modules.
Webpack Build Processing
Capturing Errors
You can listen to the done cycle via a plugin to get error information and handle the reporting logic yourself
class ErrorReportPlugin {
constructor(options) {
this.reportUrl = options.url // The target URL for reporting
}
apply(compiler) {
compiler.hooks.done.tap('ErrorReportPlugin', (stats) => {
if (stats.hasErrors()) {
const errors = stats.toJson().errors
const formattedErrors = errors.map(error => ({
message: error.message,
module: error.moduleName,
stack: error.stack,
timestamp: new Date().toISOString(),
}))
this.sendErrorsToServer(formattedErrors)
}
})
}
async sendErrorsToServer(errors) {
try {
await fetch(this.reportUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ errors }),
})
} catch (e) {
console.error('Report failed:', e)
}
}
}
// Use the plugin in the Webpack configuration
module.exports = {
plugins: [
new ErrorReportPlugin({
url: 'https://your-error-report-api.com/collect',
}),
],
}
When calling webpack in a custom build tool, you can also get error information through the webpack function callback, without adding an extra plugin
const webpack = require('webpack')
// Import the configuration for the webpack build
const config = {}
webpack(config, (err, stats) => {
if (err) {
// There is a build error
process.exit(-1)
}
})
Smoke Testing
Sometimes the build may not report errors, but the generated output has problems. You can check the output with simple scripts; common checking methods include:
- Check whether the html file with the specified name is output in the specified path
- Check whether static asset files such as js and css are generated in the specified assets directory
- Start a local preview service, access the URL, and check whether the page displays normally and the console has no errors
Webpack Build Performance Optimization
Build performance is essentially execution performance; without replacing the build tool (such as using rsbuild), it ultimately comes down to finding ways to optimize NodeJS execution performance.
Analyzing loader/plugin Time Consumption
Use speed-measure-webpack-plugin to measure time; it outputs the time consumed by each loader and plugin in the terminal
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin')
const smw = new SpeedMeasureWebpackPlugin()
module.exports = smw().wrap({
entry: './src/entry.js',
})
Analyzing Chunk Output
Analyzing the output helps us find whether dependency libraries are being built repeatedly; applicable when dealing with packages that need babel transformation, mainly used for output size analysis, i.e., loading performance optimization.
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
entry: './src/entry.js',
plugins: [
new BundleAnalyzerPlugin()
],
}
Upgrading NodeJS
Upgrading NodeJS can effectively improve the running speed of js scripts; the NodeJS internals are also continuously optimizing performance with version iterations.
NodeJS 22 supports enabling code caching, further improving running speed
Replacing Time-Consuming Algorithms
For example, replacing the md5 algorithm with md4 can also effectively improve performance — md4 is 7.69% faster than md5. This optimization can also be applied to network loading performance, such as replacing RSA-encrypted PKCS certificates with ECC certificates using the elliptic curve encryption algorithm.
Multi-Threaded Building
loader Processing
Older code uses the HappyPack plugin for multi-threaded building.
HappyPack's operating principle is to create a thread pool, hand all tasks to the thread pool, and the thread pool assigns different tasks to different threads; after the child threads finish processing, the results are returned to the main thread.
const HappyPack = require('happypack')
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
// If there are multiple modules to process, this can be changed to 'happypack/loader?id=xxx'
'happypack/loader',
],
},
],
},
plugins: [
new HappyPack({
// // Set the plugin corresponding to the loader, default id is 1
// id: 'xxx',
// Configure the number of threads, default is 3
threads: 4,
// Which loader to use for processing
loader: ['babel-loader']
})
],
}
HappyPack is now basically unmaintained; you can switch to the officially provided thread-loader instead.
thread-loader's principle is similar to HappyPack: each time a module is parsed, the module and its dependencies are assigned to worker threads for processing.
const path = require('path')
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
// thread-loader needs to be placed first, loaded last
// This way it can intercept other loaders during loader initialization for its own processing, avoiding redundant configuration
{
loader: 'thread-loader',
options: {
// Configure the number of threads, default is 2
workers: 3,
},
},
'babel-loader',
],
},
],
},
}
Code Minification
uglifyJS
Using the parallel-uglify-plugin / uglifyjs-webpack-plugin plugins, code can be minified in parallel; the underlying minification uses uglifyJS, so it cannot support minifying ES6 code
const ParallelUglifyPlugin = require('parallel-uglify-plugin')
const UglifyjsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
plugins: [
// parallel-uglify-plugin minifies faster, but its size and feature richness cannot match uglifyjs-webpack-plugin
new ParallelUglifyPlugin({
// uglify configuration
uglifyJS: {
output: {
beautify: false,
comments: false,
},
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true,
},
},
}),
// The UglifyJS plugin, supports enabling parallel minification; the output size is smaller than `parallel-uglify-plugin`, with richer features
new UglifyjsPlugin({
uglifyOptions: {
warnings: false,
parse: {},
compress: {},
mangle: true,
output: null,
toplevel: false,
nameCache: null,
ie8: false,
keep_fnames: false,
},
parallel: true,
}),
],
}
terser
webpack 4 uses terser to minify code by default, supporting ES6 code minification; parallel minification can be enabled via terser-webpack-plugin
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4,
})
],
},
}
Code Splitting
By making part of the code that doesn't change easily external, the number of modules to process is reduced, which can effectively improve build speed.
Converting to CDN
With html-webpack-externals-plugin, dependency packages can be converted to be fetched from a CDN; at runtime they are packages loaded in UMD format.
If there are many dependency packages to load, requests can easily be blocked; it is suitable for externalizing a small number of packages.
const HtmlExternalsPlugin = require('html-webpack-externals-plugin')
module.exports = {
plugins: [
new HtmlExternalsPlugin({
externals: [
{
// Package name
module: 'vue',
// Which CDN to load from instead
entry: '//cdn.jsdelivr.net/npm/vue@3.5.13/dist/vue.global.prod.js',
// The property name when mounted on Window
global: 'Vue',
},
],
})
],
}
Converting to DLL
DLL technology builds infrequently changing dependency libraries into DLL static files, so those dependency packages do not need to be processed in every build.
DLL technology supports merging packages, merging multiple related packages into the same chunk.
// webpack.dll.config.js
const webpack = require('webpack')
module.exports = {
// Essentially a DLL is a built bundle, so the build scope of the corresponding packages needs to be added to entry
// Configuring multiple entries corresponds to multiple dlls
entry: {
// Base framework package
library: [
'vue',
'vue-router',
'pinia',
],
// Chart package
chart: [
'echart',
],
},
output: {
// Configure the entry filename of the dependency library; the browser cache can be invalidated via hash
filename: '[name]_[chunkhash].dll.js',
path: './build/library',
library: '[name]',
},
plugins: [
new webpack.DllPlugin({
name: '[name]_[hash]',
// Output in manifest.json format as the package's entry
// It contains the package's description information, such as which packages there are, corresponding entries, etc.
path: './build/library/[name].json',
}),
],
}
In real projects, the DLL is referenced with the following code
// webpack.prod.config.js
const webpack = require('webpack')
module.exports = {
plugins: [
// Reference the previously built library entry
new webpack.DllReferencePlugin({
manifest: require('./build/library/library.json')
}),
// Reference the previously built chart entry
new webpack.DllReferencePlugin({
manifest: require('./build/library/chart.json')
}),
],
}
Build Caching
Purpose: improve the speed of subsequent builds
Caching babel-loader
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
// Parameters can be passed via the query parameter to enable caching
use: 'babel-loader?cacheDirectory=true',
},
],
},
}
Caching terser-webpack-plugin
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4,
// Just enable it directly here
cache: true,
})
],
},
}
Caching Module Resolution Results
Module resolution is used frequently in builds; it needs to map shorthand module names to concrete absolute paths on disk, and supports features like omission and parent directory recursion, so it is quite time-consuming overall.
const HardSourcePlugin = require('hard-source-webpack-plugin')
module.exports = {
plugins: [
new HardSourcePlugin()
],
}
cache-loader
cache-loader can cache loader processing results, used for loaders that do not provide cache functionality themselves
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader', 'babel-loader'],
},
],
},
}
webpack.cache
In webpack5, cache-loader was deprecated; the cache configuration is built in directly, letting webpack decide the cache target itself without manual configuration.
module.exports = {
// Equivalent to `cache: { type: 'memory' }`, cached in memory
// Can be set to `cache: { type: 'filesystem' }` to cache to disk
cache: true,
}
Narrowing the Build Target
Purpose: process as few modules as possible
For example, excluding packages inside node_modules by default
Excluding Third-Party Packages from Processing
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: 'babel-loader',
// Packages in node_modules are not processed by babel
exclude: 'node_modules',
},
],
},
}
Reducing Package Retrieval Levels
const path = require('path')
module.exports = {
resolve: {
// Only search in this directory, no longer going further up
modules: [
path.resolve(__dirname, 'node_modules')
],
},
}
Specifying Package mainFields
module.exports = {
resolve: {
// When reading third-party packages, only the main field needs to be recognized; conditional judgment logic such as exports and require does not need to be processed
mainFields: 'main',
},
}
Reducing extensions
module.exports = {
resolve: {
// When the suffix is omitted, only try to match .js files, not other suffixes
extensions: ['.js'],
},
}
alias
const path = require('path')
module.exports = {
resolve: {
alias: {
// Directly use the minimal production package, also saving module lookup time
vue: path.resolve(__dirname, './node_modules/vue/dist/vue.esm-browser.prod.js'),
},
},
}
Asset Size Optimization
Images
Format Conversion
Images occupy a very large amount of resources, especially png or svg with nested images ( for example, an svg referencing base64 code of other png images ). You can convert image formats, such as to webp, avif, jpeg
Or provide APIs for obtaining different image sizes through BFF, handling it by preloading low-pixel images.
Compression
If conversion really isn't possible, or the size is still large after conversion, image compression technology can be used to losslessly/lossily compress the images.
const ImageLoader = require('image-webpack-loader')
module.exports = {
module: {
rules: [
{
test: /\.(png|svg|jpe?g|gif|webp)$/,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
},
// optipng is for png compression; pngquant is used here instead of optipng
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75
}
},
},
],
},
],
},
}
CSS tree-shaking
PurifyCSS
Implementation principle: traverse the code and find the CSS that is used
Requires mini-css-extract-plugin + purgecss-webpack-plugin
const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
const PATHS = {
src: path.join(__dirname, 'src'),
}
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
},
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,
},
},
},
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
new PurgeCSSPlugin({
// Must be absolute paths; array-type values are supported
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
],
}
uncss
Implementation principle: open the page with a headless browser and use document.querySelector to identify which styles are used.
uncss is hard to adapt to SPA pages, so it is rarely used in general — only used when optimizing the first screen to remove useless css for the first screen, usually as a postcss plugin: postcss([ require('uncss').postcssPlugin ])
Dynamic polyfill
babel-Transpiled Code
Set the minimum supported version through babel and hard-code the polyfill into the output; suitable for intranet users, such as banking, finance, etc.
- Advantage: simple to use
- Disadvantages:
- Every time the minimum supported version is upgraded, a rebuild is required
- Users with newer browsers also have to bear the polyfill overhead, including loading, parsing, etc.
polyfill-service
Apply polyfills according to the user's browser version, reducing the waste of ineffective code.
The browser version is generally obtained through the UA, and can be handled with polyfill-service
Common polyfill-service providers include polyfill.io, but it is not recommended to use the service they provide, as malicious content has been detected in it — deploying the service yourself is recommended
Or use https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?version=4.8.0 / https://polyfill-fastly.io/v3/polyfill.min.js
If users modify their UA or use modified browsers, causing the loaded polyfill to still report errors, you can capture the global error and dynamically add a script with the full polyfill as a fallback.
Webpack Startup Process
The webpack Command
When the webpack command is triggered, it looks for webpack-cli / webpack-command. If neither is installed, it offers to install one automatically, and in the end only one of the two packages is used.
webpack-cli
webpack-cli mainly collects command-line arguments, converts them into webpack configuration, and calls webpack to bundle.
Some commands do not require bundling, so webpack-cli has a built-in NON_COMPILATION_ARGS array containing commands that should not trigger a webpack build, such as init / info.
When merging arguments, arguments specified on the command line take precedence over the configuration in webpack.config.js.
After merging, the compiler is created via compiler = webpack(options).
Depending on serve / build, it decides whether to call compiler.watch() or compiler.run().
tapable
Webpack hooks are basically managed by the tapable library; the core compiler and compilation both inherit from tapable.
tapable is a library similar to EventEmitter, mainly controlling the publish-subscribe of hooks.
It can be said that the whole of webpack is an event-stream-based programming paradigm: different hooks are triggered at different stages, and once a hook finishes executing, the main flow continues.
Supported Hooks
Synchronous Hooks
SyncHook: synchronous hook.SyncBailHook: synchronous bail hook. Areturnimmediately returns, preventing subsequent unhandled callbacks.SyncWaterfallHook: synchronous waterfall hook. Passes the hook's result to the next hook.SyncLoopHook: synchronous loop hook. A listener returningtruemeans continue looping; returningundefinedends the loop.
Asynchronous Hooks
AsyncParalleHook: asynchronous parallel hook.AsyncParalleBailHook: asynchronous parallel bail hook.AsyncSeriesHook: asynchronous series hook.AsyncSeriesBailHook: asynchronous series bail hook.AsyncSeriesWaterfallHook: asynchronous series waterfall hook.
Methods for Binding Hooks
| Operation | Asynchronous hooks | Synchronous hooks |
|---|---|---|
| Bind | tapAsync / tapPromise / tap |
tap |
| Execute | callAsync / promise |
call |
webpack plugin
A webpack plugin can be seen as an operation that dynamically adds hook listeners; the timing and arguments of hook triggers are built into webpack.
The entire plugin system is a complete mechanism for dynamically updating strategies, centered on the strategy pattern.
webpack Build Process
Looking at the hook call order, the basic flow is as follows:
---
title: Call order of webpack's main hooks
---
graph TB
entry["entry-option <br>( initialize options )"]
run["run <br>( start compilation )"]
make["make <br>( recursively analyze dependencies from the entry, build each dependent module )"]
beforeResolve["before-resolve <br>( resolve the module's corresponding path )"]
buildModule["build-module <br>( start building a module )"]
normalModuleLoader["normal-module-loader <br>( compile the module loaded by loaders into an AST )"]
program["program <br>( traverse the AST to collect dependencies )"]
seal["seal <br>( build finished, start optimizing code )"]
emit["emit <br>( output build artifacts to the file system )"]
entry --> run
run --> make
make --> beforeResolve
beforeResolve --> buildModule
buildModule --> normalModuleLoader
normalModuleLoader --> program
program --> seal
seal --> emit
loader
Definition: a loader is just a function that takes file content as input and outputs JS code.
The main purpose of loaders is to parse files that webpack cannot understand (non-JS / JSON) into modules webpack can recognize, unifying all resources under the same module standard, which helps webpack process them uniformly and extend support for new file types.
Loaders are always called from right to left. In some cases, a loader only cares about the metadata after the request and ignores the previous loader's result. Before actually executing the loaders (right to left), the pitch methods on the loaders are called from left to right.
loader-runner
loader-runner can run loaders without installing webpack, making it convenient to develop and debug loaders.
import { runLoaders } from "loader-runner"
runLoaders(
{
// which file to process; must be an absolute path, may include query parameters
resource: "/abs/path/to/file.txt?query",
// loaders to invoke; supported values are the same as module.rules.use in webpack
// local loaders need absolute paths
loaders: ["/abs/path/to/loader.js?query"],
// declare that only the basic context is used
context: { minimize: true },
// the loader's processing result is passed back through this function, usable for output, testing, etc.
// function signature is `function(context, path, function(err, buffer))`
processResource: (loaderContext, resourcePath, callback) => { ... },
// how to read the file; you can modify the file-reading operation for interception, testing, etc.
// function signature is `function(path, function(err, buffer))`
readResource: fs.readFile.bind(fs)
},
/**
* @param {Error | undefined} err
* @param {Object} result
* @param {Buffer | string} result.result result exists only when there is no err
* @param {Buffer} result.resourceBuffer raw Buffer data
* @param {boolean} result.cacheable whether cacheable
* @param {string[]} result.fileDependencies dependent files
* @param {string[]} result.missingDependencies missing files
* @param {string[]} result.contextDependencies context-dependent files
* @returns {void}
*/
function (err, result) {
},
)
Creating a loader
Creation: either use the webpack generate-loader command provided by webpack, or create the corresponding file yourself.
The loader-utils package is usually used to conveniently operate on loaders, such as getting options.
Synchronous loader:
const LoaderUtils = require('loader-utils')
module.exports = function (source) {
// loader options can be obtained here
const { name: _name } = LoaderUtils.getOptions(this)
// there are two ways to handle errors:
// 1. throw the error directly
throw new Error()
// 2. return it via callback
this.callback(
/* err: Error | null */ null,
/* content: string | Buffer */ source,
/* sourceMap?: SourceMap */
/* meta?: any */
)
// a synchronous loader can return source directly
// when multiple values need to be returned, callback must be used
// so it is recommended to consistently use callback
// return source
}
Asynchronous loader:
module.exports = async function (source) {
// an asynchronous loader needs to get the corresponding callback
const callback = this.async()
await new Promise((resolve) => {
resolve(true)
})
callback(
/* err: Error | null */ null,
/* content: string | Buffer */ source,
)
}
Loader Caching
By default webpack caches loaders; a loader with dependencies cannot be cached.
To manually disable caching, call this.cacheable(false) inside the loader.
Emitting Files
A loader can emit additional files directly via this.emitFile, which is why loaders like file-loader can output files.
const LoaderUtils = require('loader-utils')
module.exports = function (content) {
// get the relative path of the output file
const url = LoaderUtils.interpolateName(this, '[hash].[ext]', { content })
// emit to a file
this.emitFile(url, content)
// change the file content to the path of the corresponding module
// __webpack_public_path__ is a global environment variable, replaced with the output path
const path = `__webpack_public_path__ + ${JSON.stringify(url)}`
return `export default ${path}`
}
plugin
A plugin must run inside webpack; it cannot run standalone like a loader.
Creating a plugin
Creation: either use the webpack generate-plugin command provided by webpack, or create the corresponding file yourself.
const { RawSource } = require('webpack-sources')
module.exports = class {
constructor(options) {
// options validation can be done here; if validation fails, just throw an error
throw new Error('invalid options')
this.options = options
}
// entry point when the plugin is invoked
apply(compiler) {
compiler.plugin('emit', (compilation, cb) => {
// if an error occurs during the call phase, it is generally recorded on compilation
compilation.warnings.push('warning message')
compilation.errors.push('error message')
// emit a file; only a raw file is emitted here — there are others such as files with sourcemaps, see [webpack-sources](https://github.com/webpack/webpack-sources?tab=readme-ov-file#rawsource)
compilation.assets['filename'] = new RawSource('file content')
cb()
})
}
}
Plugins of plugins
As mentioned earlier, webpack's core is entirely event-based. If a plugin also registers events, you can develop a plugin for that plugin based on those events. For example, html-webpack-plugin provides many hooks.
By listening to these hooks, the plugin can be enhanced.
Creating Plugins
Preparation Phase
The preparation phase initializes the Compiler.
Then it decides whether to run watch or run based on the configuration.
compiler.run
Compiler also inherits from Tapable and has its own hook mechanism.
compiler.run triggers the beforeRun hook -> run hook -> compiler function in sequence.
Entry Handling
When the entry-option hook is triggered, the EntryOptionPlugin callback is invoked to check the entry configuration. If it is a function, DynamicEntryPlugin is called to register the entry; otherwise EntryPlugin is used.
The full code can be found at lib/EntryOptionPlugin.js.
Option Handling
Listens to the beforeRun hook.
Converts each options parameter into webpack built-in plugins, and registers built-in plugins according to the runtime environment, for example:
output.library:LibraryTemplatePluginexternals:ExternalsPlugindevtool:EvalDevtoolModulePlugin/SourceMapDevToolPluginAMDPlugin/CommonJsPluginRemoveEmptyChunksPlugin
The full code can be found at lib/WebpackOptionsApply.js.
Initializing compilation
A compilation is initialized for each compilation; in watch mode, every code change also re-initializes a compilation.
The initialization happens when compiler.compile is called, that is, after the run hook is triggered.
Build Phase
compiler.compile triggers beforeCompile -> compile -> make -> finishMake in sequence.
Module
Webpack abstracts all modules into Module. The specific differences are as follows:
NormalModule: regular resource files such as JS, CSS, images, read through loaders.ContextModule: handles dynamic require statements, such as modules loaded viarequire.context().ExternalModule: external dependencies, such as libraries included via CDN. This class does not inherit fromModule, because it does not need to be built.DelegateModule: handles delegated modules, such as DLL loading.RawModule: uses the file content directly as the imported content. This class does not inherit fromModule, because it does not need to be built.
make hook
The main building happens in the make phase. Taking a single-entry build as an example:
A single-entry build is converted to SingleEntryPlugin, which is EntryPlugin. As seen in the code, once triggered, this plugin calls compilation.addEntry, officially starting the build.
During the build, the build function of each different Module is called to perform the build.
- Instantiate
loaderContext, and read file information viarunLoader - After reading the module information, parse it into an AST via
parser.parse()— acron is used for JS modules, and@webassemblyjs/wasm-parseris used for WASM modules - After all modules are parsed, they are stored in
compilation.modules, triggeringfinishModules
webpack 4 used successModules instead of finishModules
Chunk Generation
By default, chunks are generated through the following process — the chunks in the final artifacts are the result after seal. The specific generation process is:
- webpack first generates a new chunk for each module corresponding to the entry
- Traverses the module's dependency list and adds the dependent modules into the chunk
- If a dynamically imported module is depended on, a new chunk is created for that module, and traversal continues
- Until all chunks are generated
Code Optimization ( seal )
This phase performs tree-shaking and other optimizations on the build artifacts. The specific execution order is:
- Build the chunk graph
- Optimize dependencies
- Optimize modules
- Optimize chunks
- tree-shaking
- Optimize ChunkModules
- Output Assets
Output Phase
Build artifacts are output via createChunkAssets, and written to the file system in emitAsset.
Complete Hook Reference Table
The compiler object triggers the following hooks in sequence:
| Hook name | Description |
|---|---|
environment |
Triggered before reading the Webpack configuration file |
afterEnvironment |
Triggered after the Webpack configuration file has been read and parsed |
entryOption |
Triggered before processing the entry options |
beforeRun |
Triggered before running the compiler, after all plugins' apply methods have been called |
run |
Starts the main compilation flow; the Compilation object is created at this stage |
watch-run (watch mode) |
Triggered when source code changes and recompilation starts |
normal-module-factory |
Creates NormalModuleFactory; very useful for plugins that handle module loading logic |
before-compile |
Triggered before creating the Compilation object |
compile |
Creates the Compilation object |
make or thisCompilation |
Triggered immediately after a new Compilation object is created, for resource and module processing |
after-compile |
Triggered after a Compilation process completes |
should-emit |
Decides whether the compilation result should be written to disk |
emit |
Triggered before the compilation result is output to disk; output content can be modified or replaced |
after-emit |
Triggered after the compilation result has been successfully output to disk; often used for cleanup or other post-processing |
done |
Triggered when the entire compilation process completes, regardless of errors |
failed |
Triggered if an error occurs during compilation |
The compilation object triggers in sequence:
| Hook name | Description |
|---|---|
build-module |
Triggered when a module starts building |
normal-module-loader |
Triggered when a module is loaded with normal loaders |
module-asset |
Triggered whenever a module adds an asset to the compilation |
optimize-module-order |
Triggered before optimizing module order |
optimize-modules-basic |
Triggered in the basic module optimization phase |
optimize-module-ids |
Triggered when assigning unique identifiers (IDs) to modules |
after-optimize-module-ids |
Triggered after module ID optimization completes |
optimize-chunk-ids |
Triggered when assigning unique identifiers to chunks |
after-optimize-chunk-ids |
Triggered after chunk ID optimization completes |
record-modules |
Triggered before recording all module information |
record-chunks |
Triggered before recording all chunk information |
before-hash |
Triggered before computing the compilation's hash |
content-hash |
Triggered when computing content-related hash values |
after-hash |
Triggered after the compilation's hash has been computed |
optimize-chunk-assets |
Triggered before optimizing chunk assets |
optimize-assets |
Triggered before optimizing all assets |
additional-chunk-assets |
Triggered after additional chunk assets are added to the compilation |
additional-assets |
Triggered after other additional assets are added to the compilation |
need-additional-pass |
Triggered if the compilation needs another pass |
before-seal |
Triggered before sealing the compilation |
optimize |
Triggered before executing the final optimization steps |
after-optimize |
Triggered after all optimization steps have been executed |
seal |
Triggered when the compilation is sealed (can no longer be modified) |
chunk-graph |
Triggered when the chunk graph is available |
after-seal |
Triggered after the compilation is fully sealed |
optimize-tree-chunk |
Chunk-level optimization for tree shaking |
optimize-tree-module |
Module-level optimization for tree shaking |
after-optimizing |
Triggered after all optimization operations complete |
revive-modules |
Triggered after modules are restored from cache |
optimize-module-factory |
Triggered when optimizing the module factory creation process |
optimize-module-factories |
Triggered after all module factory optimizations complete |
before-module-ids |
Triggered before assigning ids to modules |
module-id |
Triggered when each module id is determined |
after-module-ids |
Triggered after all module ids are assigned |
revive-chunks |
Triggered after chunks are restored from cache |
optimize-chunks-basic |
Triggered in the basic chunk optimization phase |
optimize-extracted-chunks |
Triggered when optimizing extracted chunks |
after-optimize-extracted-chunks |
Triggered after optimizing extracted chunks completes |
optimize-chunk-contents |
Triggered when optimizing chunk content |
before-hash-for-chunk |
Triggered before computing the hash of an individual chunk |
async |
Triggered after asynchronous tasks complete |
child-compiler |
Triggered when a child compiler is created |
Code Splitting
Glossary
Webpack internally contains three types of Chunk:
Initial Chunk: Chunks generated based on the Entry configurationAsync Chunk: asynchronous module references, such as the async Chunk corresponding to an import(xxx) statementRuntime Chunk: a Chunk containing only runtime code
Default Strategy
Mainly about the code-splitting strategy of SplitChunksPlugin. SplitChunksPlugin decides whether to split based on three dimensions: module reference frequency, chunk size, and number of requests for the chunk. These decisions can all be customized via the optimization.splitChunks configuration. Based on these dimensions, you can bundle content of certain specific paths separately.
SplitChunksPlugin has two built-in configuration groups, default and defaultVendors, providing some out-of-the-box features:
- Resources in
node_moduleshit thedefaultVendorsrule and are bundled separately - Only
Chunks larger than20kbare bundled separately - The number of requests required to load an
Async Chunkmust not exceed30 - The number of requests required to load an
Initial Chunkmust not exceed30
Splitting by Module Usage Frequency
SplitChunksPlugin supports deciding whether to split based on the number of times a Module is referenced by Chunks; developers can set the minimum reference count via optimization.splitChunks.minChunks.
"The number of times referenced by Chunks" is not directly equivalent to the number of times it is imported — it depends on whether the upstream caller is treated as an Initial Chunk or an Async Chunk.
Splitting by Request Count
The "request count" here refers to the number of sub-chunks that must be loaded synchronously when loading a Chunk.
For example, for a Chunk A, if i child Chunks are split out according to the splitting rules (such as module reference count, third-party packages),
then when requesting A, the browser needs to request all child Chunks at the same time. The parallel request count equals the i sub-chunks plus the A main chunk, i.e. i+1.
On top of satisfying minChunks, you can also limit the number of splits via the maxInitialRequest / maxAsyncRequests options. Semantics of the options:
maxInitialRequest: sets the maximum number of parallel requests for anInitial ChunkmaxAsyncRequests: sets the maximum number of parallel requests for anAsync Chunk
Summary of the key logic for parallel request counts:
- An
Initial Chunkitself counts as one request - An
Async Chunkdoes not count as a parallel request - The
runtimesplit out viaruntimeChunkdoes not count as a parallel request - If two
Chunks both satisfy the splitting rules, but the value ofmaxInitialRequests(ormaxAsyncRequest) only allows splitting one more module, the larger module is split first
Splitting by Size
On top of satisfying minChunks and maxInitialRequests, SplitChunksPlugin further decides whether to split based on Chunk size:
minSize: onlyChunks exceeding this size are formally splitmaxSize:Chunks exceeding this size will attempt to be split furthermaxAsyncSize: similar tomaxSize, but only applies to asynchronously imported modulesmaxInitialSize: similar tomaxSize, but only applies to entry modules configured inentryenforceSizeThreshold:Chunks exceeding this size are forcibly split, ignoring the othersizelimits above
Using cacheGroup
SplitChunksPlugin provides the cacheGroups option to set different rules for different file groups.
test: accepts a regular expression, function, or string; allModules orChunks matching thetestcondition are assigned to this grouptype: accepts a regular expression, function, or string; similar totest, it filters modules for the group, but the difference is that it judges by file type rather than file name — for example,type = 'json'matches allJSONfilesidHint: string type, used to set theChunk ID; it is also appended to the final artifact file name — for example, whenidHint = 'vendors', the output file name looks likevendors-xxx-xxx.jspriority: numeric type, used to set the priority of this group; if a module hits multiple cache groups, it is preferentially assigned to the group with the largerpriority
// set the vendors cache group via the cacheGroups property,
// all modules matching the vendors.test rule are treated as the vendors group,
// and the splitting configs such as minChunks and minSize under that group are applied first
module.exports = {
// ...
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
minChunks: 1,
minSize: 0,
},
},
},
},
}
Glossary
Entry: the compilation entry, the starting point of webpack compilationCompiler: the compilation manager; after webpack starts, acompilerobject is created and stays alive until the process exitsCompilation: the manager of a single compilation process; for example, whenwatch = true, there is only onecompilerduring the run, but every file change that triggers recompilation creates a newcompilationobjectDependence: the dependency object; webpack records inter-module dependency relationships based on this typeModule: all resources inside webpack exist asmoduleobjects; all operations, transpilation, and merging of resources are performed withmoduleas the basic unitChunk: when compilation is complete and ready for output, webpack organizesmodules intochunks according to specific rules; thesechunks correspond one-to-one with the final output to some extentLoader: a resource content transformer — essentially a transformer that converts content A into BPlugin: during the webpack build process, corresponding events are broadcast at specific moments; plugins listen to these events and intervene in the compilation process at specific points in time
sourcemap
The data structure is as follows:
{
/** The version of the source map. */
"version": 4,
/** The name of the transformed file. */
"file": "out.js",
/** The directory of the source files. Empty if in the same directory as the transformed file. */
"sourceRoot": "",
/** The source files. This is an array, indicating that multiple files may have been merged. */
"sources": ["foo.js", "bar.js"],
/** All variable names and property names before transformation. */
"names": ["src", "maps", "are", "fun"],
/** The string recording position information. */
"mappings": "AAgBC,SAAQ,CAAEA"
}
vite VS webpack
vitecan configure thebuild.sourcemapoption to controlsourcemap; the possible values are simplified toinline,hidden,true,falsetrue: generates thesourcemapto a separate file and appends a//# sourceURLcomment after the module (i.e. webpack'ssource-map)inline: generates thesoucemapand embeds it into the file as a DataURI (i.e. webpack'sinline-source-map)hidden: generates thesourcemapto a separate file, but does not append a comment to the artifact (i.e. webpack'shidden-source-map)false: does not generate asourcemap
webpackcan configure thedevtooloption to controlsourcemap; the possible values areeval: executes each module withevaland appends a//# sourceURLcomment after the moduleeval-cheap-source-map: generates asourcemapwithout column mappings, and executes modules withevaleval-cheap-module-source-map: similar toeval-cheap-source-map, but generatessourcemaps for the modules'loaderseval-source-map: generates a completesourcemap, and executes modules withevalcheap-source-map: generates asourcemapwithout column informationcheap-module-source-map: similar tocheap-source-map, but generatessourcemaps for the modules'loadersinline-cheap-source-map: generates asourcemapwithout column information, and embeds it into the file as a DataURIinline-cheap-module-source-map: similar toinline-cheap-source-map, but generatessourcemaps for the modules'loaderssource-map: generates a completesourcemapfileinline-source-map: generates a completesourcemap, and embeds it into the file as a DataURIhidden-source-map: generates a completesourcemapfile, but does not add a link comment to the artifactnosources-source-map: generates asourcemapfile, but without the source code content
Security and Performance Optimization
You can specify the SourceMap by configuring the HTTP request header, and apply permission control at the same time — returning this header only for specific users to avoid source code leakage.
For example, in k8s, it can be configured via Ingress annotations:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
# this is the key part: check the condition and add the corresponding SourceMap header
if ($http_cookie ~* "tag=123") {
add_header SourceMap "$request_uri.map";
}
spec:
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backend-service
port:
number: 80