Supabase Edge Function 冷启动慢:原因 + 修复路径

第一次请求 1–5 秒、暖请求只要 100ms。定位真正原因(大依赖、顶层 await、区域)并缩短启动时间。

你部署了一个 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-1ap-southeast-1eu-west-1eu-central-1us-east-1us-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 冷启动)

如何确认已修好

  1. 闲置 ~15 分钟后重跑 Step 1 的”冷-暖”计时。现在 cold 应该已经接近 warm(瘦身后的 function 通常在 ~500ms 以内)。
  2. Edge Functions → Logs / Metrics 里盯几天 boot time——要消失的是那些高离群点,而不只是平均值降一点。
  3. 如果加了保温 cron,去 Integrations → Cron → 运行记录 确认它在按时触发,并且这些调用都快速返回了 ok

预防建议

  • 设计时就估算冷启动可接受度:后台同步可以慢,用户实时交互必须 < 500ms
  • 对延迟敏感的 endpoint:小项目用 cron 保温,大流量换 V8 isolate 平台。
  • 模块顶层只放声明——Deno.serve 之前零异步 import、零 fetch、零长计算。
  • 不需要的依赖别引;优先原生 fetchjsr:,别用大 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 验证。

相关阅读

标签: #独立开发 #排查 #排查