你部署了一个 Supabase Edge Function,本地用 supabase functions serve 跑稳定 ~100ms。生产环境闲一会儿之后第一次请求要 1-5 秒,后面再请求又恢复 100-200ms,空闲一段又慢回去。这就是冷启动:流量停下来后,Supabase 把你的 Deno isolate 释放掉,下一次请求得重新 spawn 一个新 isolate、evaluate 你的代码、解析 import、跑完顶层初始化,才能开始响应。
最快的修复思路: 启动开销几乎都来自你自己的代码,而不是平台。Supabase 自己的基准测试(2025 年 7 月持久化存储那次发布)把平台层面的冷启动压到平均 ~42ms,P99 从 ~15s 降到 ~460ms。所以 2026 年还出现好几秒的冷启动,基本意味着你的 function 在启动时干了重活——通常是一个大 npm: import 或一个顶层 await。把这些活儿挪出启动路径(下面的 Step 3-4),你就能拿回大部分延迟。如果只想今晚先把慢的问题压下去,加个 cron 保温(Step 2)。
常见原因
按命中率从高到低:
1. Function 长时间无请求被 evict
最后一次请求之后,Supabase 会保留一个暖 isolate 一段时间(时长跟套餐有关),然后把它 evict 掉。下次请求就触发完整冷启动。没有公开的”保活 N 分钟”参数可以配置——只要 function 几分钟没流量,就当它是冷的。
如何判断:用户报”凌晨 / 周末偶尔很慢”——典型的低流量时段冷启动。(另外注意:免费档的项目整体在 7 天零活动后会被暂停,那是另一个慢得多的问题。)
2. import 了大依赖(特别是 npm 包)
import { Anthropic } from 'npm:@anthropic-ai/sdk'; // ~500KB
import { OpenAI } from 'npm:openai'; // ~1MB
import _ from 'npm:lodash'; // ~70KB
CLI 会把 function 打包成 ESZip,但一个庞大的 npm: 依赖图在冷 isolate 上加载和 evaluate 仍然实打实地耗时——通常 1-3 秒。这是 2026 年导致好几秒启动最常见的单一原因。
如何判断:function 顶部有多个 npm: import,或者 deno.json / import_map.json 里指向了大包。
3. 模块顶层做了重活
// 顶层——每次 cold start 都会重跑
const config = await fetch('https://...').then(r => r.json());
const dbConnection = await connectDB();
Deno.serve(async (req) => { ... });
每次 isolate 启动这些都重跑,叠加在冷启动本身之上。
如何判断:文件顶部有 await、有顶层的 client / 连接,或者在 Deno.serve 之前有长同步计算。
4. cold start 时打外部 API
Deno.serve(async (req) => {
// cold start 时这个 fetch 也算"冷"的
const data = await fetch('https://slow-third-party.com/data');
...
});
upstream 自己慢,会叠加在冷启动之上,第一个请求把两份延迟都吃了。
如何判断:handler 里第一个 await 是你控制不了的外部 API。
5. 打包了大 JSON / WASM
一个 5MB 的 JSON 数据集或一个 WASM 模块——parse / instantiate 都耗时,而且会占用 20MB 的打包体积上限(截至 2026 年 6 月)。
如何判断:functions 目录下有大 .json / .wasm 文件,或者打包体积接近 20MB。
6. 区域不匹配(用户在 Asia、function 在 US)
冷启动本身 + 跨区域往返,在用户那里就是 5+ 秒。默认情况下 Edge Function 会在离调用方最近的区域执行,但如果你固定了某个区域、或者项目主区域离用户很远,延迟就会叠加。
如何判断:响应头 x-sb-edge-region(或 function 内部的 SB_REGION 环境变量)跟用户所在区域对不上。
我属于哪一类?
| 症状 | 最可能原因 | 跳到 |
|---|---|---|
| 只在几分钟没流量后慢,平时很快 | 空闲 evict(#1) | Step 2 |
第一次请求总是慢,有好几个 npm: import | 大依赖(#2) | Step 3 |
连暖请求都偏慢;文件顶部有 await | 顶层初始化(#3) | Step 4 |
| 第一个请求慢,能追溯到某个 upstream 调用 | 冷的外部调用(#4) | Step 5 |
| 只有部分地区的用户慢 | 区域(#6) | Step 6 |
最短修复路径
Step 1:测量真实冷启动
# 等 ~15 分钟无请求,然后第一次请求计时
time curl -X POST "https://xyz.supabase.co/functions/v1/my-fn" \
-H "Authorization: Bearer $KEY"
# 紧接着对暖请求计时
time curl -X POST "https://xyz.supabase.co/functions/v1/my-fn" \
-H "Authorization: Bearer $KEY"
cold 减 warm 的差值就是你真实的冷启动代价。再到 Dashboard 里交叉验证:Edge Functions → 你的 function → Logs / Metrics,那里每次调用都记录了 boot time 和 x-sb-edge-region。如果暖请求本身就慢,那你是普通的延迟问题,不是冷启动——先修 handler。
Step 2:cron 保温(最简单,先争取时间)
如果能接受额外的 invocations,就按固定间隔 ping 一下 function,让 isolate 保持暖。Supabase Cron 用的是 pg_cron + pg_net:
-- Supabase Dashboard → Integrations → Cron(或者直接在 SQL editor 里跑)
SELECT cron.schedule(
'keep-warm',
'*/5 * * * *', -- 每 5 分钟
$$ SELECT net.http_post(
url := 'https://xyz.supabase.co/functions/v1/my-fn',
headers := '{"Content-Type": "application/json", "Authorization": "Bearer ANON_KEY"}'::jsonb,
body := '{"warmup": true}'::jsonb
); $$
);
让 function 识别这个 warmup ping 并立刻返回,避免跑真正的业务逻辑:
const body = await req.json().catch(() => ({}));
if (body.warmup) return new Response('ok');
成本:每月 ~8,640 次额外 invocations(5 分钟一次)。Free 档自带的 Edge Functions 额度比较充足,所以通常不花钱;如果想把间隔压到 5 分钟以下,先确认套餐里包含的调用次数。这只是治标——配合 Step 3-4 才能真正缩短启动。
Step 3:依赖瘦身
最大的杠杆。把能用原生 fetch 调的 npm: SDK 直接丢掉:
// 全量 SDK——冷启动时会拉一大坨模块图
import Anthropic from 'npm:@anthropic-ai/sdk';
// 原生 fetch——没有 import 开销
async function callAnthropic(messages: any[]) {
return fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': Deno.env.get('ANTHROPIC_API_KEY')!,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages,
}),
});
}
每个 SDK 换成原生 fetch 通常能省 1-2 秒启动。确实需要包的地方,优先用 Deno 原生或 jsr: 包,别用 npm:;体积大的就用 handler 里的动态 import() 懒加载,只在真正用到时才加载:
Deno.serve(async (req) => {
const { default: heavyLib } = await import('npm:some-heavy-lib');
...
});
Step 4:把顶层初始化挪进 handler
// 顶层——每次 cold start 都重跑
const config = await loadConfig();
Deno.serve(async (req) => { ... });
// handler 里 lazy load + cache
let configPromise: Promise<Config> | null = null;
Deno.serve(async (req) => {
configPromise ??= loadConfig(); // 只在第一次请求时 load
const config = await configPromise;
...
});
isolate 暖着的时候 configPromise 已经 resolve,不会重复 load。这样启动路径就只剩下定义一个函数 + 调 Deno.serve,正好是 Supabase 优化得最好的路径。
Step 5:把慢的外部调用挪出响应路径
如果某个慢的 upstream 调用不需要阻塞响应,就用 EdgeRuntime.waitUntil(Supabase 的全局对象)在响应发出之后再执行,这样用户就不用既等冷启动、又等慢的第三方:
Deno.serve(async (req) => {
const result = await quickProcess(req);
// 响应发出之后才执行,不阻塞用户
// @ts-expect-error EdgeRuntime is a Supabase global
EdgeRuntime.waitUntil(slowExternalCall());
return Response.json(result);
});
Step 6:在对的区域运行(别动整个项目区域)
跨区域延迟会和冷启动叠加。你通常不需要改项目区域——固定 function 跑在哪个区域就行。截至 2026 年 6 月,可以按请求指定执行区域:
// Supabase 客户端(JS)
import { FunctionRegion } from '@supabase/supabase-js';
const { data, error } = await supabase.functions.invoke('my-fn', {
region: FunctionRegion.ApNortheast1,
});
# 或者用请求头
curl -X POST 'https://xyz.supabase.co/functions/v1/my-fn' \
-H 'x-region: ap-northeast-1'
# 或者用 query 参数(无法设请求头时,比如 webhook、CORS 预检)
# ...functions/v1/my-fn?forceFunctionRegion=ap-northeast-1
可选值包括 ap-northeast-1、ap-southeast-1、eu-west-1、eu-central-1、us-east-1、us-west-1 等(完整列表见区域调用文档)。用响应头 x-sb-edge-region 确认。
只有当你那个重数据库的 function 确实必须紧贴一个很远的主库时,才考虑改项目区域(Dashboard → Project Settings → General → Region)——这个操作慢、且不容易回退,先评估清楚。
Step 7:如果冷启动是业务命门,就换平台
当亚 100ms 的首字节是硬指标、Step 1-6 还不够时,V8 isolate 平台比全新的 Deno isolate 启动更快:
- Cloudflare Workers——V8 isolate,冷启动接近于零
- Vercel Edge Functions——类似的 isolate 模型
- 永远 warm 的 Node serverless——Fly.io Machine、Railway(进程一直在跑,根本没有 isolate 冷启动)
如何确认已修好
- 闲置 ~15 分钟后重跑 Step 1 的”冷-暖”计时。现在 cold 应该已经接近 warm(瘦身后的 function 通常在 ~500ms 以内)。
- 在 Edge Functions → Logs / Metrics 里盯几天 boot time——要消失的是那些高离群点,而不只是平均值降一点。
- 如果加了保温 cron,去 Integrations → Cron → 运行记录 确认它在按时触发,并且这些调用都快速返回了
ok。
预防建议
- 设计时就估算冷启动可接受度:后台同步可以慢,用户实时交互必须
< 500ms。 - 对延迟敏感的 endpoint:小项目用 cron 保温,大流量换 V8 isolate 平台。
- 模块顶层只放声明——
Deno.serve之前零异步import、零fetch、零长计算。 - 不需要的依赖别引;优先原生
fetch或jsr:,别用大npm:包,大包就懒加载。 - 大 JSON / WASM 别打进 bundle,放 Supabase Storage 或外部 KV 按需读取;bundle 远离 20MB 上限。
- 监控 p99 latency 时把 cold 和 warm 分开统计,对高冷启动比例告警。
- 用户主要集中在某个区域,就用
x-region把 function 固定到那里,别靠猜。
常见问题
为什么本地快、只有生产慢?
supabase functions serve 整个会话只保持一个暖 isolate,所以你永远看不到冷启动。生产环境会在流量间隙把 isolate evict 掉,所以闲置后的第一个请求要付完整启动开销。
Supabase 不是把冷启动提速了 97% 吗?我的怎么还慢?
2025 年 7 月那次改动修的是平台层面的启动开销(平均 ~870ms → ~42ms,P99 ~15s → ~460ms)。它对你自己代码在启动时花的时间毫无帮助——大依赖和顶层 await 还是得你自己解决(Step 3-4)。
升 Pro 套餐能消除冷启动吗? 不能。付费档抬高的是各种上限(比如 wall-clock 时长从 Free 的 150s 升到付费的 400s)、并把 isolate 暖着的时间稍微拉长,但不会消除冷启动。任何套餐下,靠谱的办法都是保温 ping 或更瘦的 function。
cron 保温违反 Supabase 条款吗?是不是很浪费?
这是官方支持的模式(pg_cron + pg_net),每月几千次极小的调用可以忽略不计。只要让 function 在 warmup body 上短路返回,别真把业务逻辑跑 8,640 次就行。
当前我该按哪些 Edge Function 上限来设计?
截至 2026 年 6 月:256MB 内存、每请求 ~2s CPU 时间、Free 档 150s wall-clock(付费档 400s)、150s 请求空闲超时(否则返回 504)、20MB 打包体积上限。
怎么在不动数据库的前提下改 function 运行区域?
按请求指定即可:客户端 region 选项、x-region 请求头、或 forceFunctionRegion query 参数,都不用改项目。用响应头 x-sb-edge-region 验证。