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 时:

  1. Cloudflare 创建一个新的 Isolate(或唤醒休眠的)
  2. 下载并解析你的 Worker 代码
  3. 执行模块级代码(包括顶层 import 和顶层 await
  4. 开始处理第一个请求

步骤 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-parsesharp、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)或手写
momentdate-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、内存管理。