V8 启动快照
Egg 可以把一个完全加载好的应用固化成 V8 启动快照, 从而让冷启动跳过绝大部分启动开销。构建快照时会加载整个模块图——框架元数据、 插件、Service、Router 和 tegg 模块——并把生命周期跑到 configWillLoad,再把此时的 堆序列化成一个 blob。恢复 blob 时只需继续执行剩余生命周期(didReady)并开始监听, 进程几乎可以立即对外服务。
它构建在 Bundle 部署 之上:快照是从自包含 bundle 产出的,因此可以把 快照理解为「预启动好的 bundle」。单进程模式会生成一个 bundle 和一个 blob;cluster 模式会分别生成 app 与 agent bundle,每个角色都有独立的 blob 和 V8 堆。
Node.js 版本要求
| 阶段 | 命令 | Node.js |
|---|---|---|
| 构建快照 | egg-bin snapshot build | >= 22 |
| 启动普通 cluster bundle | 不提供角色 blob 的 egg-scripts start --bundle | >= 22 |
| 恢复快照堆 | --snapshot-blob,或提供角色 blob 的 --bundle | >= 24 |
恢复必须使用 Node.js >= 24
快照可以在 Node.js >= 22 上构建,但在 Node.js 22 上恢复一个非平凡的 Egg 堆时, 进程会在反序列化阶段以原生 fatal 错误崩溃(Check failed: current == end_slot_index, 属于 V8 的 bug)。请始终在 Node.js >= 24 上恢复。
受支持的启动方式会同时拦截单进程 --snapshot-blob,以及提供了角色 blob 的 cluster --bundle 启动:Node.js < 24 时会在启动任何进程前直接报清晰错误并拒绝启动。如果你 绕过它、在 Node.js 22 上直接运行 node --snapshot-blob,进程仍会在反序列化阶段以上面的 原生 fatal 崩溃——快照自带的运行时拦截只能在「能完成反序列化但仍低于 24」的版本上打印 友好提示。因此请始终通过 egg-scripts 在 Node.js >= 24 上恢复。
使用 CLI 构建与恢复
推荐使用 bundler CLI。注意没有 egg-bin snapshot start:构建属于构建期(egg-bin), 恢复属于生产运行期(egg-scripts)。
构建 blob
# 以快照模式打包(单文件自包含 worker.js + prelude),并自动执行
# `node --snapshot-blob <blob> --build-snapshot worker.js`
$ egg-bin snapshot build默认会把 bundle 写到 ./dist-bundle,blob 写到 ./dist-bundle/snapshot.blob。常用参数:
为 cluster 模式构建相互独立的 app 和 agent 快照:
$ egg-bin snapshot build --cluster默认会在 ./dist-bundle 下生成 app_worker.js、agent_worker.js、 app.snapshot.blob 和 agent.snapshot.blob。常用参数:
| 参数 | 说明 |
|---|---|
--output <dir> | bundle 输出目录。 |
--cluster | 分别构建 app、agent worker bundle 和 blob。 |
--blob <path> | 单进程 blob 路径,默认 <output>/snapshot.blob。 |
--app-snapshot-blob <path> | cluster app blob 路径,默认 <output>/app.snapshot.blob。 |
--agent-snapshot-blob <path> | cluster agent blob 路径,默认 <output>/agent.snapshot.blob。 |
--force-external | 始终保持 external 的包,可重复(见「已知限制」)。 |
--skip-bundle | 从已有的 snapshot-ready worker 入口重建 blob,跳过再次打包。 |
恢复并提供服务
直接用 egg-scripts 从 blob 启动进程(Node.js >= 24):
$ egg-scripts start --snapshot-blob ./dist-bundle/snapshot.blob --port 7001它会启动一个单文件自包含的 node --snapshot-blob <blob> 进程(没有 egg-cluster,也 不做框架解析)。快照主函数从 PORT(或 --port)读取监听端口,执行 snapshotDidDeserialize 钩子,然后调用 app.listen()。
cluster 模式需要启用 bundle worker 入口,并提供一个或两个角色的 blob:
$ egg-scripts start --bundle \
--app-snapshot-blob ./dist-bundle/app.snapshot.blob \
--agent-snapshot-blob ./dist-bundle/agent.snapshot.blob--bundle-dir 默认是 ./dist-bundle。app 和 agent blob 相互独立:只提供其中一个时, 对应角色从 blob 恢复,另一个角色仍从生成的 bundle JavaScript 正常启动。使用 snapshot blob 的 cluster worker 必须采用 process 模式;不带 blob 的普通 cluster bundle 也支持 worker_threads。
对外 API
如果需要自定义单进程入口文件,Egg 也从 egg 导出两个方法:
import { buildSnapshot, restoreSnapshot } from 'egg';buildSnapshot()会以快照模式启动 Egg,加载元数据,触发非可序列化资源的清理钩子, 并把应用对象写入 V8 快照负载。restoreSnapshot()会从快照中恢复应用,重建运行期资源,并继续执行剩余的 Egg 生命周期。
构建入口:
import { buildSnapshot } from 'egg';
await buildSnapshot({
baseDir: import.meta.dirname,
});node --snapshot-blob=snapshot.blob --build-snapshot snapshot-entry.mjs恢复入口(Node.js >= 24):
import { restoreSnapshot } from 'egg';
const app = await restoreSnapshot();
await app.listen(7001);restoreSnapshot() 会从 configDidLoad 继续执行正常启动流程直到 didReady,因此返回的 app 已经可以继续执行运行期初始化逻辑,例如启动服务或建立外部连接。
这些方法只表示一个应用堆。cluster 工作流由 CLI 生成和恢复 app、agent 两个角色入口。
工作原理
在构建阶段,Egg 会以 snapshot: true 运行。此时 Egg 会加载应用元数据,但在 configWillLoad 之后停止,因此 configDidLoad、didLoad、willReady、didReady 和 serverDidReady 等钩子都会延后到恢复阶段执行。在序列化堆之前,Egg 会运行 snapshotWillSerialize 钩子,释放不可序列化的资源(timer、socket、原生句柄、logger 流)。网络栈(node:http、node:https、TLS/DNS、HTTP client)会保持 external 且 惰性加载,因此它们不可序列化的原生绑定不会被写进 blob,而是在恢复后首次使用时重建。
恢复阶段,V8 先反序列化堆,随后快照主函数运行 snapshotDidDeserialize 钩子重建这些 运行期资源,跑完延后的生命周期直到 didReady,然后开始监听。
cluster 模式下,app 和 agent worker 会独立执行这套流程。每个 blob 捕获由对应角色入口 构建出的堆;每个恢复进程也只继续该角色的生命周期和通信协议。
保持 external 且惰性加载的模块集合默认是 Node 网络栈(http、https、http2、tls、 dns)、inspector 和 cluster(含它们的 node: 形式),以及 undici、urllib。 如果该列表之外的某个 builtin 或包在 import 时初始化了原生状态,可以通过应用 package.json 里的 egg.snapshot.lazyModules 扩展这个集合:
{
"egg": {
"snapshot": {
"lazyModules": ["node:zlib"]
}
}
}当某个第三方依赖或 builtin 破坏了构建或恢复时,参见 快照故障排查,了解如何定位罪魁祸首模块并修复。
快照生命周期钩子
如果你的 app.js 或 agent.js Boot 类管理了不能直接写入 V8 快照的资源,可以实现下面 两个钩子:
class AppBootHook {
async snapshotWillSerialize() {
// 在写入快照前关闭或解绑不可序列化资源
}
async snapshotDidDeserialize() {
// 在恢复后重新创建这些资源
}
}
module.exports = AppBootHook;snapshotWillSerialize()会在写入快照前执行。snapshotDidDeserialize()会在进程从快照启动后执行。
这两个钩子适合处理 timer、socket、process listener、logger 等需要在真实运行期重新建立 的资源。
性能
由于模块图已经加载、应用也已启动到 configWillLoad,恢复阶段主要执行 configDidLoad 之后的运行期初始化、didReady 与连接/监听。下面在 cnpmcore 4.32.1、Node.js 24.18.1、Apple M1 Pro、prod 环境下,对同一份生成的 JavaScript 产物进行实测;每种模式预热 1 次,再交错测量 10 次。 单进程计时范围为直接 spawn Node.js 到开始监听;Cluster 使用 process 模式、1 个 agent + 2 个 app worker,并采用 master 内部 ready 计时,排除 launcher 和 master 引导开销:
| 进程模型 | Bundle 中位数 | Snapshot 中位数 | 提升 |
|---|---|---|---|
| 单进程 | 947 ms | 379 ms | 快 2.50 倍,降低 59.9% |
| Cluster(2 app) | 1356 ms | 591 ms | 快 2.30 倍,降低 56.5% |
已知限制
- 恢复需要 Node.js >= 24(见上文)。
- Cluster snapshot blob 仅支持 process 模式:普通 cluster bundle 支持
worker_threads,但 Node 无法在 worker thread 内恢复自定义 V8 启动 blob。 - 不支持 snapshot 与 bundle cluster 启动模块:使用
options.require的--snapshot-blob或 cluster bundle 会在创建 worker 前直接报错。 - 原生 addon 为 external,必须在部署目标上存在。
- 第三方依赖受限:任何在模块求值阶段就打开活跃资源或捕获不可序列化状态的依赖 (打开的 socket、原生 HTTP/2 绑定、后台 timer、文件句柄)都必须要么保持 external (
--force-external),要么实现快照生命周期钩子,在序列化前释放、在恢复后重建。并不是 每个包都开箱即可被快照化。 - Web 全局对象必须在调用处引用:undici 支撑的全局对象 (
fetch/Headers/Request/Response/FormData/WebSocket/……)会在构建期被替换为桩 (触碰它们会拉起 undici 不可序列化的原生绑定),并在恢复时重新安装,因此在调用处使用时可正常工作。 但在模块求值期捕获的绑定——const f = fetch,或class X extends globalThis.Request——会把构建期的 桩固化进 blob 且不会被升级。请在使用处引用 Web 全局对象,不要在模块顶层捕获。
支持范围仍在演进中;完整的已知限制与设计取舍记录在项目的 V8 快照 RFC 中。
故障排查
如果快照无法构建(序列化期间原生中止)或无法恢复,参见 快照故障排查。其中涵盖了构建期与恢复期的错误特征、 如何定位捕获了不可序列化状态的模块,以及可用的修复手段 (--force-external、egg.snapshot.lazyModules、生命周期钩子)。