Webpack 的冷启动慢一直是老生常谈的问题。一个包含上千个模块的项目,光是把所有入口和依赖全部编译一遍就可能花掉一两分钟,而开发阶段真正被访问到的页面往往只有其中一小部分。lazyCompilation 的出现改变了这个局面:它把编译时机从启动阶段推迟到模块被实际请求的那一刻,入口文件、动态导入的模块在第一次被浏览器或服务端访问时才会真正编译。本文将详细介绍这一特性的配置方式,并重点讨论服务端组件场景下的实践细节。

一、lazyCompilation 的基本原理与最小配置
lazyCompilation 的工作机制并不复杂。启用之后,Webpack 在构建阶段不会真正产出所有模块的代码,而是为每个入口和异步模块生成一个极小的占位模块(stub)。当应用运行起来,某个模块被真实请求时,这个占位模块会先向编译后端发一个信号,Webpack 收到信号后增量编译该模块,再让页面重新加载拿到真正的代码。也就是说,第一次访问某个路由会稍慢一点,但换来的是启动时间的大幅缩短。
最小配置非常简单,只需在 webpack.config.js 中开启 experiments.lazyCompilation:
module.exports = {
// 其他配置...
experiments: {
lazyCompilation: true
}
};
传 true 等价于同时开启 lazyCompilation.enter 和 lazyCompilation.imports 两项。前者针对入口文件,即所谓的 lazy entry,把主入口变成按需编译;后者针对动态导入的模块(比如通过 import() 拆分的代码块)。你可以单独控制其中一项:
module.exports = {
experiments: {
lazyCompilation: {
imports: true, // 异步 chunk 懒编译
entry: false // 入口不启用,保持正常编译
}
}
};
如果你的项目入口只有一个,但异步路由非常多,只开 imports 是比较稳妥的选择,因为入口仍然第一时间可用,页面的首屏行为不会发生任何变化,而那些还没被访问到的路由 chunk 则完全不参与首次编译。
二、服务端组件场景下的进阶配置
服务端渲染(SSR)或服务端组件框架下使用 lazyCompilation 需要多一份注意。原因是懒编译依赖一个后端服务(backend)来接收模块激活请求,默认实现在浏览器端通过发起请求通知 Webpack,但服务端环境里没有浏览器,模块的激活信号来源变成了服务端自身的模块加载行为。如果 backend 没有正确接管,就会出现模块永远不被编译、页面一直等待的死循环。
先看一个完整的 SSR 场景配置示例:
const webpack = require('webpack');
module.exports = {
target: 'node', // 服务端构建
experiments: {
lazyCompilation: {
imports: true,
entry: false,
backend: {
// 服务端环境下用 http 方式与编译进程通信
client: require.resolve(
'webpack/lazyCompilation-backend?server'
)
}
}
},
plugins: [
// 开发模式下才需要,生产构建不要带
process.env.NODE_ENV === 'development' && new webpack.HotModuleReplacementPlugin()
].filter(Boolean)
};
backend.client 指定了负责发送激活信号的客户端脚本路径,默认值是 webpack/lazyCompilation-backend,它内部会根据运行环境自动选择用 fetch 还是 Node 的 http 模块发请求。你也可以通过 query 参数自定义端口,例如 lazyCompilation-backend?port=1235。在 monorepo 或者编译进程与运行进程分离的架构中,还需要指定 backend.listen 为一个 net.Server 实例,让多个构建共享同一个通信通道,避免端口冲突。
另一个需要关注的点是循环依赖。服务端组件之间引用关系往往比纯前端项目复杂,懒编译生成的占位模块会改变模块初始化的时序。如果一个模块在被激活前就被同步 require,占位模块会返回空对象,可能触发 undefined is not a function 之类的错误。解决办法是给这类模块加入 test 过滤,把它们排除在懒编译范围之外:
experiments: {
lazyCompilation: {
imports: true,
test: module => {
// 排除基础库和有循环依赖的核心模块
return !/node_modules/.test(module.resource || '')
&& !/core\/bootstrap/.test(module.resource || '');
}
}
}
test 接收模块对象,返回 false 表示跳过懒编译、按常规方式编译。合理使用它可以规避绝大多数时序问题。
三、常见问题排查与生产环境策略
第一个高频问题是启用后动态导入不生效,控制台里看到请求打到了类似 /__lazy_compilation__ 的代理路径上。这其实是正常现象——占位模块通过这个路径上报激活信号,随后页面会自动 reload。如果页面没有自动刷新,多半是 HMR 配置缺失或者 devServer.webSocketServer 与激活请求被代理层拦截了,检查 devServer 的 proxy 规则,确保代理路径没有吞掉懒编译的请求。
第二个问题是与 React Fast Refresh 的兼容性。lazyCompilation 在某些版本组合下会与 Fast Refresh 的模块热替换产生冲突,表现为组件状态被意外重置。建议保持 webpack-cli 和 webpack-dev-server 为较新版本,并在必要时将入口编译关掉只保留 imports 懒编译,因为冲突多发生在入口代理环节。
最后也是最关键的一点:lazyCompilation 只应该存在于开发环境。它产出的代码依赖运行时通信机制,绝不能进入生产构建。推荐用环境变量做开关:
const isDev = process.env.NODE_ENV === 'development';
module.exports = {
experiments: {
lazyCompilation: isDev ? { imports: true, entry: false } : false
}
};
配合 cache: { type: 'filesystem' } 持久化缓存一起使用效果更佳,二者叠加后,冷启动从分钟级降到秒级是完全可行的。如果团队从零接入,建议先只在 imports 维度开启,观察一到两周没有问题后再考虑对入口启用懒编译,逐步扩大收益范围。
WebpacklazyCompilation懒编译修改时间:2026-09-06 19:12:34