Webpack 构建一次动辄几分钟,很多人第一反应是加缓存或者换机器,但更合理的做法是先搞清楚时间到底花在了哪里。Webpack 在每次编译结束后会生成一个 stats 对象,其中包含一个 timings 字段,记录了整个编译过程的关键耗时信息。读懂并利用好这个字段,是做构建性能分析的第一步。本文将围绕 stats.timings 的结构、读取方式以及如何借助它定位瓶颈展开讲解。

stats.timings 到底记录了什么
当一次编译完成后,Webpack 会把编译过程中的时间数据汇总到 stats 对象的 timings 属性里。在 Webpack 4 及更早版本中,这个字段的结构比较扁平,主要包括 time(总耗时,单位毫秒)、buildTime 等键。而在 Webpack 5 中,timings 的信息更加结构化,它是一个以时间戳形式记录的对象,内部包含 startTime、buildTime 等字段,分别代表编译开始的时间戳和实际构建消耗的毫秒数。
需要注意的一点是,stats.timings 给出的是一个宏观层面的时间汇总,它告诉你这次构建总共花了多久,但不会直接告诉你每个 loader 花了多少毫秒、每个插件占用了多少时间。它更像一个总账,而不是明细账。不过这个总账非常有价值:通过在修改配置前后对比 buildTime 的变化,你可以量化每一次优化到底带来了多少收益,避免凭感觉做判断。
拿到 stats 对象的方式通常有两种。如果使用命令行,可以在 webpack.config.js 中配置 stats: 'verbose' 或在输出中包含 timing 信息;如果是通过 Node API 调用 webpack,那么回调函数的第二个参数就是完整的 stats 对象,直接访问 stats.toJson({ timings: true }) 即可拿到序列化后的 timings 数据。
用 Node API 读取并展示各阶段耗时
命令行输出的信息有限,更灵活的方式是写一个脚本来构建并解析 stats。下面是一个完整的示例,它调用 webpack 进行编译,然后从 stats 中提取 timing 相关数据并格式化输出。
const webpack = require('webpack');
const config = require('./webpack.config.js');
const compiler = webpack(config);
compiler.run((err, stats) => {
if (err) {
console.error(err);
process.exit(1);
}
// 将 stats 序列化为 JSON,显式开启 timings
const json = stats.toJson({ timings: true, assets: true, modules: false });
console.log('===== 构建耗时统计 =====');
console.log(`总耗时 buildTime: ${json.time} ms`);
if (json.timings) {
Object.keys(json.timings).forEach(key => {
const value = json.timings[key];
// startTime 是时间戳,buildTime 是毫秒数,分类展示
if (key === 'startTime') {
console.log(`开始时间: ${new Date(value).toLocaleString()}`);
} else {
console.log(`${key}: ${value} ms`);
}
});
}
compiler.close(() => {
console.log('编译器已关闭');
});
});这段代码的关键点在于 stats.toJson 的参数。timings: true 确保 timing 数据被包含在输出中,同时可以通过关闭 modules 等选项减少序列化开销。输出的 buildTime 就是这次完整编译消耗的毫秒数,把它记录到文件里,多次构建后就能形成一条趋势数据,方便观察构建速度的长期变化。
如果你希望对多个配置(比如开发环境和生产环境)分别统计,可以把上面的逻辑封装成函数,遍历配置数组依次执行,并把结果写入 JSON 文件,再配合简单的图表工具做趋势展示。这种低成本的做法在很多团队里比引入完整的构建监控平台更实用。
深入 compilation 钩子做精细化打点
stats.timings 的粒度不够细时,可以借助 Webpack 的插件机制在编译生命周期的各个钩子上打时间戳。Webpack 的 Compiler 和 Compilation 对象暴露了大量钩子,比如 make(开始构建模块)、finishMake、seal(开始封装优化阶段)、afterSeal、emit(输出资源到磁盘)等,正好对应构建的几个大阶段。
class TimingPlugin {
apply(compiler) {
const marks = {};
compiler.hooks.make.tap('TimingPlugin', () => {
marks.make = Date.now();
});
compiler.hooks.compilation.tap('TimingPlugin', (compilation) => {
compilation.hooks.finishModules.tap('TimingPlugin', () => {
marks.finishModules = Date.now();
});
compilation.hooks.seal.tap('TimingPlugin', () => {
marks.seal = Date.now();
});
compilation.hooks.afterOptimizeChunkAssets.tap('TimingPlugin', () => {
marks.afterOptimize = Date.now();
});
});
compiler.hooks.emit.tapAsync('TimingPlugin', (compilation, callback) => {
marks.emit = Date.now();
callback();
});
compiler.hooks.done.tap('TimingPlugin', () => {
marks.done = Date.now();
console.log('===== 各阶段耗时 =====');
const order = ['make', 'finishModules', 'seal', 'afterOptimize', 'emit', 'done'];
order.forEach((key, i) => {
if (i === 0) return;
const prev = order[i - 1];
const cost = marks[key] - marks[prev];
console.log(`${prev} -> ${key}: ${cost} ms`);
});
});
}
}
module.exports = TimingPlugin;把这个插件加入配置后,每次构建结束都会打印出各阶段的分段耗时。一般来说,make 到 finishModules 这段对应模块的解析和 loader 转换,往往是耗时大头;seal 之后的阶段对应 Tree Shaking、代码分割、压缩等优化流程,生产构建中这块占比会明显升高。有了分段数据,优化方向就清晰多了:模块阶段慢就治理 loader 和 resolve,优化阶段慢就调整 terser 的并行度或换用 esbuild 压缩。
与第三方工具相比,这种自研打点的方式没有额外依赖,也不会干扰构建本身的速度。像 speed-measure-webpack-plugin 这类插件虽然能给出每个 loader 和插件的耗时,但它对 Webpack 5 的兼容性曾长期不稳定,而且测量本身会带来一定的性能损耗,在 CI 环境中开启可能导致数据失真。因此建议的做法是:日常用 stats.timings 记录总耗时,需要排查问题时再临时启用详细的打点插件。
根据耗时分布制定优化策略
定位到瓶颈之后,优化就有了明确靶子。如果耗时集中在模块构建阶段,优先检查几件事:一是排除不必要的 loader 处理范围,比如让 babel-loader 只处理 src 目录并开启 exclude: /node_modules/;二是利用缓存,Webpack 5 内置的持久化缓存(cache: { type: 'filesystem' })通常能把二次构建时间压缩到原来的几分之一;三是检查 resolve 配置,合理设置 extensions 和 alias,减少模块查找的尝试次数。
如果耗时集中在 seal 之后的优化阶段,重点则不同。压缩是生产构建中最常见的慢点,可以把 terser-webpack-plugin 的 parallel 选项设为 true 利用多核,或者直接改用速度更快的压缩方案。此外,source map 的生成方式对耗时影响极大,eval-cheap-module-source-map 与 source-map 在大项目上的差距可能达到数倍,开发环境应尽量选择轻量的映射方式。
最后提醒一点,任何优化都应该用数据验证闭环。改完配置后重新跑一次构建,对比 stats.timings 中的 buildTime,确认收益真实存在再合入代码。构建优化最忌讳的是凭感觉堆砌配置,看似加了很多优化项,实际构建反而变慢了。把 stats.timings 的记录固化到 CI 流程中,让每次流水线都留下构建耗时数据,长期来看还能及时发现依赖膨胀带来的性能退化,防患于未然。
Webpackstats.timings构建耗时分析修改时间:2026-09-15 23:08:59