18.13-冷启动优化
要点
- Worker 冷启动是指 Isolate 从 0 到能处理第一个请求的过程——通常 5-50ms,但重依赖可能拖到几百 ms
- 冷启动延迟的来源:模块加载、顶层 await、大依赖解析、WASM 初始化
- Module Worker 格式下,模块级代码只在 Isolate 启动时执行一次——是冷启动的关键路径
- 优化思路:延迟加载、减少顶层 await、拆分大依赖、缓存关键依赖
- Cron Triggers 可以预热 Isolate——定期发请求防止 Isolate 被回收
- 不是所有接口都对冷启动敏感——后台任务、webhook 可以容忍,用户交互接口要优化
1. 什么是冷启动
Cloudflare Worker 运行在 V8 Isolate 里。当一个请求落到一个刚启动或很久没用的 Isolate 时:
- Cloudflare 创建一个新的 Isolate(或唤醒休眠的)
- 下载并解析你的 Worker 代码
- 执行模块级代码(包括顶层
import和顶层await) - 开始处理第一个请求
步骤 1-3 的时间就是「冷启动延迟」——第一个请求的用户要等这段时间。
后续请求复用同一个 Isolate,不需要重新启动——这就是「热启动」。
2. 冷启动延迟的来源
2.1 模块加载
每个 import 语句都要解析和执行被导入的模块。模块依赖树越大,加载越慢。
// 这个 import 链很长
import { Hono } from 'hono' // 1 个模块
import { z } from 'zod' // 1 个模块
import { OpenAI } from 'openai' // 1 个模块(但内部依赖很多)
import { PDFParse } from './lib/pdf-parse' // 你自己的模块,可能依赖一堆东西Cloudflare 会缓存解析后的模块,但第一次加载时还是要全跑一遍。
2.2 顶层 await
Module Worker 支持顶层 await——模块加载时可以做异步操作:
// 顶层 await:Isolate 启动时执行一次
const config = await loadConfigFromKV()
const models = awaitloadModelList()顶层 await 会阻塞模块加载——如果 loadConfigFromKV() 需要 20ms,冷启动就多了 20ms。
2.3 大依赖
一些大型库(pdf-parse、sharp、AI SDK)解析和执行成本高。特别是包含 WASM 的库——WASM 初始化的开销比纯 JS 大。
2.4 代码体积
Worker 代码越大(包含依赖),下载和解析越慢。Cloudflare 有代码体积限制(免费 1MB,付费 10MB),但体积和启动时间正相关。
3. 优化策略
3.1 减少顶层 await
把「启动时加载」改成「第一次使用时加载」:
// 不好:顶层 await 阻塞冷启动
const config = await loadConfigFromKV()
export function getConfig() {
return config
}
// 好:懒加载
let configCache: Config | null = null
export async function getConfig(env: Env): Promise<Config> {
if (!configCache) {
configCache = await loadConfigFromKV(env)
}
return configCache
}懒加载的代价是第一个请求会慢——但只有第一个请求慢,不影响冷启动时间。
3.2 选择性导入
只导入需要的函数,不要导入整个库:
// 不好:导入整个 lodash
import _ from 'lodash'
const result = _.get(obj, 'a.b.c')
// 好:只导入需要的函数
import get from 'lodash/get'
const result = get(obj, 'a.b.c')现代打包工具(esbuild、Turbopack)会自动 tree-shaking,但显式导入更安全。
3.3 拆分大依赖
把大依赖放到单独的动态 import 里——只在需要时加载:
// src/routes/pdf.ts
app.post('/api/pdf/parse', async (c) => {
// 动态 import:只有这个路由被访问时才加载
const { parsePDF } = await import('../lib/pdf-parse')
const file = await c.req.arrayBuffer()
const result = await parsePDF(file)
return c.json(result)
})动态 import 的模块会被 Cloudflare 单独打包。只有访问这个路由时才会加载,其他路由的冷启动不受影响。
3.4 避免在模块顶层做重计算
// 不好:模块加载时跑重计算
const embeddings = await Promise.all(
documents.map(doc => computeEmbedding(doc))
)
// 好:懒加载,第一次使用时才算
let embeddingsCache: number[][] | null = null
async function getEmbeddings() {
if (!embeddingsCache) {
embeddingsCache = await Promise.all(
documents.map(doc => computeEmbedding(doc))
)
}
return embeddingsCache
}4. 依赖体积控制
4.1 查看依赖体积
# 使用 esbuild 分析
npx wrangler deploy --dry-run --outdir=dist
ls -la dist/4.2 轻量替代
| 重度依赖 | 轻量替代 |
|---|---|
lodash(完整) | lodash-es(tree-shakable)或手写 |
moment | date-fns(tree-shakable) |
axios | 原生 fetch |
zod(大) | valibot(更小) |
pdf-parse | 云端 API(移除本地依赖) |
4.3 WASM 依赖
WASM 模块初始化成本比纯 JS 高。如果可能,用纯 JS 实现替代:
// 不好:WASM 图像压缩(初始化慢)
import { compress } from 'wasm-image-compress'
// 好:纯 JS 实现(虽然慢一点,但不阻塞冷启动)
import { compress } from 'pure-js-image-compress'或者把 WASM 依赖动态 import——只在需要时加载。
5. Cron Triggers 预热
用 Cron Triggers 定期发请求,防止 Isolate 被回收:
// wrangler.jsonc
{
"triggers": {
"crons": ["*/5 * * * *"] // 每 5 分钟
}
}// src/index.ts
export default {
async fetch(request: Request, env: Env) {
// 正常处理请求
},
async scheduled(event: ScheduledEvent, env: Env) {
// 定期预热:给自己发一个请求
await fetch('https://your-worker.example/health', {
headers: { 'X-Warmup': 'true' },
})
},
}预热不能消除冷启动,但可以减少冷启动的频率——Isolate 存活时间更长。
6. 监控冷启动
6.1 看响应时间分布
export default {
async fetch(request: Request, env: Env) {
const start = Date.now()
const response = await handleRequest(request, env)
const duration = Date.now() - start
// 记录到日志或分析系统
console.log(JSON.stringify({
path: new URL(request.url).pathname,
duration,
coldStart: duration > 100, // 粗判断:超过 100ms 可能是冷启动
}))
return response
},
}6.2 Cloudflare Analytics
Cloudflare Dashboard → Workers & Pages → 选你的 Worker → Metrics 能看到响应时间分布。P95 和 P99 显著高于 P50 说明有冷启动问题。
7. 接口分级
不是所有接口都对冷启动敏感:
| 接口类型 | 冷启动敏感度 | 优化策略 |
|---|---|---|
| 用户交互(聊天、搜索) | 高 | 预热、懒加载、减少依赖 |
| Webhook 接收 | 中 | 可以接受 100ms 延迟 |
| 后台任务 | 低 | 无需优化 |
| 定时任务 | 低 | 本身就是定期启动 |
关键路径(用户直接交互的接口)要重点优化;非关键路径可以容忍冷启动。
8. Hono 特定优化
8.1 路由注册
Hono 的路由注册是 O(1) 的——路由多少不影响冷启动。但中间件注册会执行中间件工厂函数:
// 这个中间件工厂在模块加载时执行
app.use('*', logger()) // logger() 在模块加载时调用如果中间件工厂函数很重,考虑延迟初始化。
8.2 轻量 Hono
hono/tiny 是更小的 Hono 版本——去掉了一些不常用的功能,体积更小:
import { Hono } from 'hono/tiny' // 比 'hono' 小9. 小结
冷启动优化核心:
- 减少顶层 await:改成懒加载,第一个请求时再初始化
- 选择性导入:只导入需要的函数,不要导入整个库
- 拆分大依赖:用动态 import,只在需要时加载
- 控制依赖体积:用轻量替代(
date-fns代替moment,原生fetch代替axios) - WASM 谨慎:初始化成本高,能不用就不用,或动态加载
- Cron Triggers 预热:定期发请求防止 Isolate 被回收
- 接口分级:关键路径重点优化,非关键路径可以容忍
冷启动不是敌人——合理的懒加载和预热能让 99% 的请求都热启动。剩下的 1% 冷启动延迟,大多数场景也能接受。
下一篇讲边缘部署性能优化——边缘计算的特点、CPU 时间优化、流式处理、并行 IO、内存管理。