Edge Functions:是什么、何时用、何时不用
Edge 运行时、V8 Isolate、冷启动的真相、地理路由、A/B 测试、边缘认证,以及为什么我把一些东西搬回了 Node.js。关于边缘计算的平衡视角。
"Edge" 这个词现在被滥用得厉害。Vercel 在说,Cloudflare 在说,Deno 也在说。每场关于 Web 性能的技术分享都不可避免地提到"在边缘运行",仿佛这是一个能让你的应用瞬间变快的魔法咒语。
我曾经也深信不疑。我把 middleware、API 路由,甚至一些渲染逻辑都搬到了 edge runtime。其中一些决定确实很明智,但还有一些,我在凌晨两点调试连接池错误三周后,又悄悄搬回了 Node.js。
这篇文章就是这段经历的平衡版本——edge 到底是什么,它在哪里真正发光,在哪里完全不行,以及我如何为应用的每个部分选择运行时。
什么是 Edge?#
先从地理开始说起。当有人访问你的网站时,请求会从他们的设备出发,经过 ISP,穿过互联网到达你的服务器,经过处理后,响应再沿原路返回。如果你的服务器在 us-east-1(弗吉尼亚),而用户在东京,这一趟往返大约是 14,000 公里。光在光纤中的传播速度,单程大约就是 70ms——仅仅是物理层面。加上 DNS 解析、TLS 握手和处理时间,用户看到第一个字节之前轻松就要 200-400ms。
"Edge" 的意思就是在全球分布的服务器上运行你的代码——也就是那些一直以来都在提供静态资源的 CDN 节点,只不过现在它们也能执行你的逻辑了。不再是弗吉尼亚的一台源服务器,而是你的代码在全球 300 多个位置运行。东京的用户访问东京的服务器,巴黎的用户访问巴黎的服务器。
延迟的计算简单而令人信服:
Traditional (single origin):
Tokyo → Virginia: ~140ms round trip (physics alone)
+ TLS handshake: ~140ms more (another round trip)
+ Processing: 20-50ms
Total: ~300-330ms
Edge (local PoP):
Tokyo → Tokyo edge node: ~5ms round trip
+ TLS handshake: ~5ms more
+ Processing: 5-20ms
Total: ~15-30ms
这是初始响应的 10-20 倍提升。这是真实的、可测量的,对于某些操作来说是颠覆性的。
但营销话术掩盖了一点:edge 并不是一个完整的服务器环境,它是根本不同的东西。
V8 Isolates vs Node.js#
传统的 Node.js 运行在一个完整的操作系统进程中。它能访问文件系统,能打开 TCP 连接,能启动子进程,能以流的方式读取环境变量,基本上能做 Linux 进程能做的任何事。
Edge functions 不运行在 Node.js 上。它们运行在 V8 isolates 上——也就是驱动 Chrome 的那个 JavaScript 引擎,但被精简到了核心。可以把 V8 isolate 想象成一个轻量级沙盒:
// This works in Node.js but NOT at the edge
import fs from "fs";
import { createConnection } from "net";
import { execSync } from "child_process";
const file = fs.readFileSync("/etc/hosts"); // ❌ No filesystem
const conn = createConnection({ port: 5432 }); // ❌ No raw TCP
const result = execSync("ls -la"); // ❌ No child processes
process.env.DATABASE_URL; // ⚠️ Available but static, set at deploy time在 edge 上你能用的是 Web API 接口——和浏览器中一样的那些 API:
// These all work at the edge
const response = await fetch("https://api.example.com/data");
const url = new URL(request.url);
const headers = new Headers({ "Content-Type": "application/json" });
const encoder = new TextEncoder();
const encoded = encoder.encode("hello");
const hash = await crypto.subtle.digest("SHA-256", encoded);
const id = crypto.randomUUID();
// Web Streams API
const stream = new ReadableStream({
start(controller) {
controller.enqueue("chunk 1");
controller.enqueue("chunk 2");
controller.close();
},
});
// Cache API
const cache = caches.default;
await cache.put(request, response.clone());这些限制是真实而严格的:
- 内存:每个 isolate 128MB(Cloudflare Workers),某些平台为 256MB
- CPU 时间:10-50ms 的实际 CPU 时间(不是挂钟时间——
await fetch()不算,但JSON.parse()解析 5MB 数据就算) - 没有原生模块:任何需要 C++ 绑定的东西(bcrypt、sharp、canvas)都用不了
- 没有持久连接:你无法在请求之间保持数据库连接
- 打包体积限制:整个 worker 脚本通常 1-5MB
这不是 CDN 上的 Node.js,而是一个拥有不同思维模型的不同运行时。
冷启动:神话与现实#
你大概听说过 edge functions 有"零冷启动"。这……基本上是真的,而且对比确实很惊人。
传统的基于容器的 serverless 函数(AWS Lambda、Google Cloud Functions)是这样工作的:
- 请求到达
- 平台分配一个容器(如果没有可用的)
- 容器启动操作系统
- 运行时初始化(Node.js、Python 等)
- 你的代码加载并初始化
- 请求被处理
第 2-5 步就是冷启动。对于 Node.js Lambda,通常是 200-500ms。对于 Java Lambda,可能是 2-5 秒。对于 .NET Lambda,500ms-1.5s。
V8 isolates 的工作方式不同:
- 请求到达
- 平台创建一个新的 V8 isolate(或复用一个温热的)
- 你的代码加载(部署时已经编译为字节码)
- 请求被处理
第 2-3 步不到 5ms,通常不到 1ms。Isolate 不是容器——没有操作系统要启动,没有运行时要初始化。V8 在微秒级别就能创建一个新的 isolate。"零冷启动"是营销话术,但现实(低于 5ms 的启动)已经足够接近零,对大多数场景来说无关紧要。
但冷启动在以下情况仍然会咬你一口:
大打包体积。 如果你的 edge function 引入了 2MB 的依赖,这些代码仍然需要加载和解析。我吃过亏——当我把一个校验库和一个日期格式化库打包进 edge middleware 后,冷启动从 2ms 涨到了 40ms。仍然很快,但不是"零"了。
冷门节点。 Edge 服务商有几百个 PoP,但不是所有 PoP 都能保持你的代码活跃。如果你每小时只从内罗毕收到一个请求,那个 isolate 在请求之间就会被回收。下一个请求又得重新付出启动成本。
单个请求多个 isolate。 如果你的 edge function 调用了另一个 edge function(或者 middleware 和 API route 都在 edge 上),你可能会为一个用户请求启动多个 isolate。
实用建议:保持 edge function 的打包体积小。只导入你需要的东西。积极做 tree-shake。打包越小,冷启动越快,"零冷启动"的承诺就越能兑现。
// ❌ Don't do this at the edge
import dayjs from "dayjs";
import * as yup from "yup";
import lodash from "lodash";
// ✅ Do this instead — use built-in APIs
const date = new Date().toISOString();
const isValid = typeof input === "string" && input.length < 200;
const unique = [...new Set(items)];Edge Functions 的完美用例#
经过大量实验后,我发现了一个清晰的模式:edge functions 擅长的是在请求到达源服务器之前做出快速决策。它们是守门人、路由器和转换器——不是应用服务器。
1. 基于地理位置的重定向#
这是杀手级用例。请求到达最近的 edge 节点,节点已经知道用户在哪里。不需要 API 调用,不需要 IP 查询数据库——平台直接提供地理数据:
// middleware.ts — runs at the edge on every request
import { NextRequest, NextResponse } from "next/server";
export const config = {
matcher: ["/", "/shop/:path*"],
};
export function middleware(request: NextRequest) {
const country = request.geo?.country ?? "US";
const city = request.geo?.city ?? "Unknown";
const region = request.geo?.region ?? "Unknown";
// Redirect to country-specific store
if (request.nextUrl.pathname === "/shop") {
const storeMap: Record<string, string> = {
DE: "/shop/eu",
FR: "/shop/eu",
GB: "/shop/uk",
JP: "/shop/jp",
TR: "/shop/tr",
};
const storePath = storeMap[country] ?? "/shop/us";
if (request.nextUrl.pathname !== storePath) {
return NextResponse.redirect(new URL(storePath, request.url));
}
}
// Add geo headers for downstream use
const response = NextResponse.next();
response.headers.set("x-user-country", country);
response.headers.set("x-user-city", city);
response.headers.set("x-user-region", region);
return response;
}这在 5ms 内完成,就在用户身边。另一种方案——把请求一路发到源服务器做 IP 查询再重定向——对于远离源服务器的用户来说要花 100-300ms。
2. 无闪烁的 A/B 测试#
客户端 A/B 测试会导致讨厌的"原始内容闪烁"——用户先看到版本 A 一瞬间,然后 JavaScript 才替换成版本 B。在 edge 上,你可以在页面开始渲染之前就分配变体:
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Check if user already has a variant assignment
const existingVariant = request.cookies.get("ab-variant")?.value;
if (existingVariant) {
// Rewrite to the correct variant page
const url = request.nextUrl.clone();
url.pathname = `/variants/${existingVariant}${url.pathname}`;
return NextResponse.rewrite(url);
}
// Assign a new variant (50/50 split)
const variant = Math.random() < 0.5 ? "control" : "treatment";
const url = request.nextUrl.clone();
url.pathname = `/variants/${variant}${url.pathname}`;
const response = NextResponse.rewrite(url);
response.cookies.set("ab-variant", variant, {
maxAge: 60 * 60 * 24 * 30, // 30 days
httpOnly: true,
sameSite: "lax",
});
return response;
}用户永远不会看到闪烁,因为 rewrite 发生在网络层。浏览器甚至不知道这是一个 A/B 测试——它直接收到变体页面。
3. Auth Token 验证#
如果你的认证使用 JWT(而不是数据库 session 查询),edge 就是完美选择。JWT 验证是纯加密操作——不需要数据库:
import { jwtVerify, importSPKI } from "jose";
import { NextRequest, NextResponse } from "next/server";
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`;
export async function middleware(request: NextRequest) {
const token = request.cookies.get("session-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
try {
const key = await importSPKI(PUBLIC_KEY, "RS256");
const { payload } = await jwtVerify(token, key, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
});
// Pass user info downstream as headers
const response = NextResponse.next();
response.headers.set("x-user-id", payload.sub as string);
response.headers.set("x-user-role", payload.role as string);
return response;
} catch {
// Token expired or invalid
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("session-token");
return response;
}
}这个模式很强大:edge middleware 验证 token 并将用户信息作为可信 header 传递给源服务器。你的 API 路由不需要再次验证 token——只需读取 request.headers.get("x-user-id")。
4. 机器人检测和速率限制#
Edge functions 可以在恶意流量到达源服务器之前就将其拦截:
import { NextRequest, NextResponse } from "next/server";
// Simple in-memory rate limiter (per edge location)
const rateLimitMap = new Map<string, { count: number; timestamp: number }>();
export function middleware(request: NextRequest) {
const ip = request.headers.get("x-forwarded-for")?.split(",").pop()?.trim()
?? "unknown";
const ua = request.headers.get("user-agent") ?? "";
// Block known bad bots
const badBots = ["AhrefsBot", "SemrushBot", "MJ12bot", "DotBot"];
if (badBots.some((bot) => ua.includes(bot))) {
return new NextResponse("Forbidden", { status: 403 });
}
// Simple rate limiting
const now = Date.now();
const windowMs = 60_000; // 1 minute
const maxRequests = 100;
const entry = rateLimitMap.get(ip);
if (entry && now - entry.timestamp < windowMs) {
entry.count++;
if (entry.count > maxRequests) {
return new NextResponse("Too Many Requests", {
status: 429,
headers: { "Retry-After": "60" },
});
}
} else {
rateLimitMap.set(ip, { count: 1, timestamp: now });
}
// Periodic cleanup to prevent memory leak
if (rateLimitMap.size > 10_000) {
const cutoff = now - windowMs;
for (const [key, val] of rateLimitMap) {
if (val.timestamp < cutoff) rateLimitMap.delete(key);
}
}
return NextResponse.next();
}需要注意的是:上面的速率限制 map 是每个 isolate、每个节点独立的。如果你有 300 个 edge 节点,每个都有自己的 map。要做严格的速率限制,你需要分布式存储,比如 Upstash Redis 或 Cloudflare Durable Objects。但对于粗略的滥用防护,每个节点的限制效果出奇地好。
5. 请求重写和个性化 Header#
Edge functions 非常擅长在请求到达源服务器之前对其进行转换:
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const url = request.nextUrl;
// Device-based content negotiation
const ua = request.headers.get("user-agent") ?? "";
const isMobile = /mobile|android|iphone/i.test(ua);
response.headers.set("x-device-type", isMobile ? "mobile" : "desktop");
// Feature flags from cookie
const flags = request.cookies.get("feature-flags")?.value;
if (flags) {
response.headers.set("x-feature-flags", flags);
}
// Locale detection for i18n
const acceptLanguage = request.headers.get("accept-language") ?? "en";
const preferredLocale = acceptLanguage.split(",")[0]?.split("-")[0] ?? "en";
const supportedLocales = [
"en", "tr", "de", "fr", "es", "pt", "ja", "ko", "it",
"nl", "ru", "pl", "uk", "sv", "cs", "ar", "hi", "zh",
];
const locale = supportedLocales.includes(preferredLocale)
? preferredLocale
: "en";
if (!url.pathname.startsWith(`/${locale}`) && !url.pathname.startsWith("/api")) {
return NextResponse.redirect(new URL(`/${locale}${url.pathname}`, request.url));
}
return response;
}Edge 失败的场景#
这是营销页面跳过的部分。我踩过这里的每一个坑。
1. 数据库连接#
这是最大的问题。传统数据库(PostgreSQL、MySQL)使用持久 TCP 连接。Node.js 服务器在启动时打开连接池,然后跨请求复用这些连接。高效、成熟、广为人知。
Edge functions 做不到这一点。每个 isolate 都是临时的。没有"启动"阶段来打开连接。即使你能打开连接,isolate 可能在一个请求之后就被回收,浪费了连接建立的时间。
// ❌ This pattern fundamentally doesn't work at the edge
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // Connection pool of 10
});
// Each edge invocation would:
// 1. Create a new Pool (can't reuse across invocations reliably)
// 2. Open a TCP connection to your database (which is in us-east-1, not at the edge)
// 3. Do TLS handshake with the database
// 4. Run the query
// 5. Discard the connection when the isolate recycles
// Even with connection pooling services like PgBouncer,
// you're still paying the network latency from edge → origin database数据库往返的问题是根本性的。你的数据库在一个区域,而你的 edge function 在 300 个区域。每次从 edge 发起的数据库查询都必须从 edge 节点到数据库所在区域再返回。对于在东京访问东京 edge 节点的用户来说,如果数据库在弗吉尼亚:
Edge function in Tokyo
→ Query to PostgreSQL in Virginia: ~140ms round trip
→ Second query: ~140ms more
→ Total: 280ms just for two queries
Node.js function in Virginia (same region as DB)
→ Query to PostgreSQL: ~1ms round trip
→ Second query: ~1ms more
→ Total: 2ms for two queries
在这个场景下,edge function 的数据库操作慢了 140 倍。edge function 启动更快并不重要——数据库往返主导了一切。
这就是基于 HTTP 的数据库代理存在的原因(Neon 的 serverless 驱动、PlanetScale 的 fetch 驱动、Supabase 的 REST API)。它们能工作,但仍然是在向单一区域的数据库发送 HTTP 请求。它们解决了"无法使用 TCP"的问题,但没有解决"数据库离得远"的问题。
// ✅ This works at the edge (HTTP-based database access)
// But it's still slow if the database is far from the edge node
import { neon } from "@neondatabase/serverless";
export const runtime = "edge";
export async function GET(request: Request) {
const sql = neon(process.env.DATABASE_URL!);
// This makes an HTTP request to your Neon database
// Works, but latency depends on distance to the database region
const posts = await sql`SELECT * FROM posts WHERE published = true LIMIT 10`;
return Response.json(posts);
}2. 长时间运行的任务#
Edge functions 有 CPU 时间限制,通常是 10-50ms 的实际计算时间。挂钟时间更宽裕(通常 30 秒),但 CPU 密集型操作会很快触及限制:
// ❌ These will exceed CPU time limits at the edge
export const runtime = "edge";
export async function POST(request: Request) {
const data = await request.json();
// Image processing — CPU intensive
// (Also can't use sharp because it's a native module)
const processed = heavyImageProcessing(data.image);
// PDF generation — CPU intensive + needs Node.js APIs
const pdf = generatePDF(data.content);
// Large data transformation
const result = data.items // 100,000 items
.map(transform)
.filter(validate)
.sort(compare)
.reduce(aggregate, {});
return Response.json(result);
}如果你的函数需要超过几毫秒的 CPU 时间,它就应该放在区域 Node.js 服务器上。没有讨论余地。
3. 仅限 Node.js 的依赖#
这一点让很多人措手不及。数量惊人的 npm 包依赖 Node.js 内置模块:
// ❌ These packages won't work at the edge
import bcrypt from "bcrypt"; // Native C++ binding
import sharp from "sharp"; // Native C++ binding
import puppeteer from "puppeteer"; // Needs filesystem + child_process
import nodemailer from "nodemailer"; // Needs net module
import { readFile } from "fs/promises"; // Node.js filesystem API
import mongoose from "mongoose"; // TCP connections + Node.js APIs
// ✅ Edge-compatible alternatives
import { hashSync } from "bcryptjs"; // Pure JS implementation (slower)
// For images: use a separate service or API
// For email: use an HTTP-based email API (Resend, SendGrid REST)
// For database: use HTTP-based clients在把任何东西搬到 edge 之前,检查每一个依赖。一个藏在依赖树三层深处的 require("fs") 就会在运行时让你的 edge function 崩溃——不是在构建时。你部署了,一切看起来没问题,然后第一个请求命中那段代码路径时你就收到一个莫名其妙的错误。
4. 大打包体积#
Edge 平台有严格的打包体积限制:
- Cloudflare Workers:1MB(免费),5MB(付费)
- Vercel Edge Functions:4MB(压缩后)
- Deno Deploy:20MB
这听起来够用了,直到你 import 一个 UI 组件库、一个校验库和一个日期库。我曾经有一个 edge middleware 膨胀到 3.5MB,因为我从一个 barrel file 导入,结果拉进了整个 @/components 目录。
// ❌ Barrel file imports can pull in way too much
import { validateEmail } from "@/lib/utils";
// If utils.ts re-exports from 20 other modules, all of them get bundled
// ✅ Import directly from the source
import { validateEmail } from "@/lib/validators/email";5. Streaming 和 WebSocket#
Edge functions 可以做流式响应(Web Streams API),但长连接的 WebSocket 是另一回事。虽然某些平台支持 edge 上的 WebSocket(Cloudflare Workers、Deno Deploy),但 edge functions 的临时性本质使其不适合有状态的长连接。
Next.js Edge Runtime#
Next.js 让你可以非常方便地按路由选择 edge runtime。你不需要全面切换——你可以精确选择哪些路由在 edge 上运行。
Middleware(始终在 Edge)#
Next.js middleware 始终运行在 edge 上。这是设计如此——middleware 拦截每个匹配的请求,所以它需要快速且全球分布:
// middleware.ts — always runs at the edge, no opt-in needed
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// This runs before every matching request
// Keep it fast — no database calls, no heavy computation
return NextResponse.next();
}
export const config = {
// Only run on specific paths
matcher: [
"/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
],
};Edge 上的 API 路由#
任何路由处理器都可以选择 edge runtime:
// app/api/hello/route.ts
export const runtime = "edge"; // This one line changes the runtime
export async function GET(request: Request) {
return Response.json({
message: "Hello from the edge",
region: process.env.VERCEL_REGION ?? "unknown",
timestamp: Date.now(),
});
}Edge 上的页面路由#
甚至整个页面都可以在 edge 上渲染,不过我会在这样做之前仔细考虑:
// app/dashboard/page.tsx
export const runtime = "edge";
export default async function DashboardPage() {
// Remember: no Node.js APIs here
// Any data fetching must use fetch() or edge-compatible clients
const data = await fetch("https://api.example.com/dashboard", {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
next: { revalidate: 60 },
}).then((r) => r.json());
return (
<main>
<h1>Dashboard</h1>
{/* render data */}
</main>
);
}Edge Runtime 中可用的 API#
这是一份实用参考,告诉你什么能用什么不能用:
// ✅ Available at the edge
fetch() // HTTP requests
Request / Response // Web standard request/response
Headers // HTTP headers
URL / URLSearchParams // URL parsing
TextEncoder / TextDecoder // String encoding
crypto.subtle // Crypto operations (signing, hashing)
crypto.randomUUID() // UUID generation
crypto.getRandomValues() // Cryptographic random numbers
structuredClone() // Deep cloning
atob() / btoa() // Base64 encoding/decoding
setTimeout() / setInterval() // Timers (but remember CPU limits)
console.log() // Logging
ReadableStream / WritableStream // Streaming
AbortController / AbortSignal // Request cancellation
URLPattern // URL pattern matching
// ❌ NOT available at the edge
require() // CommonJS (use import)
fs / path / os // Node.js built-in modules
process.exit() // Process control
Buffer // Use Uint8Array instead
__dirname / __filename // Use import.meta.url
setImmediate() // Not a web standard边缘认证:完整模式#
我想深入讲讲认证,因为它是 edge 最有影响力的用例之一,但也很容易搞错。
有效的模式是:在 edge 验证 token,将可信的 claims 传递给下游,永远不要在 middleware 中碰数据库。
// lib/edge-auth.ts — Edge-compatible auth utilities
import { jwtVerify, SignJWT, importSPKI, importPKCS8 } from "jose";
const PUBLIC_KEY_PEM = process.env.JWT_PUBLIC_KEY!;
const ISSUER = "https://auth.myapp.com";
const AUDIENCE = "https://myapp.com";
export interface TokenPayload {
sub: string;
email: string;
role: "user" | "admin" | "moderator";
iat: number;
exp: number;
}
export async function verifyToken(token: string): Promise<TokenPayload | null> {
try {
const publicKey = await importSPKI(PUBLIC_KEY_PEM, "RS256");
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ["RS256"],
issuer: ISSUER,
audience: AUDIENCE,
clockTolerance: 30, // 30 seconds of clock skew tolerance
});
return payload as unknown as TokenPayload;
} catch {
return null;
}
}
export function isTokenExpiringSoon(payload: TokenPayload): boolean {
const now = Math.floor(Date.now() / 1000);
const fiveMinutes = 5 * 60;
return payload.exp - now < fiveMinutes;
}// middleware.ts — The auth middleware
import { NextRequest, NextResponse } from "next/server";
import { verifyToken, isTokenExpiringSoon } from "./lib/edge-auth";
const PUBLIC_PATHS = ["/", "/login", "/register", "/api/auth/login"];
const ADMIN_PATHS = ["/admin"];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip auth for public paths
if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith("/api/public"))) {
return NextResponse.next();
}
// Extract token
const token = request.cookies.get("auth-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Verify token (pure crypto — no database call)
const payload = await verifyToken(token);
if (!payload) {
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("auth-token");
return response;
}
// Role-based access control
if (ADMIN_PATHS.some((p) => pathname.startsWith(p)) && payload.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
// Pass verified user info to the origin as trusted headers
const response = NextResponse.next();
response.headers.set("x-user-id", payload.sub);
response.headers.set("x-user-email", payload.email);
response.headers.set("x-user-role", payload.role);
// Signal if token needs refresh
if (isTokenExpiringSoon(payload)) {
response.headers.set("x-token-refresh", "true");
}
return response;
}// app/api/profile/route.ts — Origin server reads trusted headers
export async function GET(request: Request) {
// These headers were set by edge middleware after JWT verification
// They're trusted because they come from our own infrastructure
const userId = request.headers.get("x-user-id");
const userRole = request.headers.get("x-user-role");
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// Now we can hit the database — we're on the origin server,
// right next to the database, with a connection pool
const user = await db.user.findUnique({ where: { id: userId } });
return Response.json(user);
}关键洞察:edge 做快的部分(加密验证),源服务器做慢的部分(数据库查询)。每个部分都在最高效的地方运行。
一个重要的注意事项:这只适用于 JWT。如果你的认证系统每次请求都需要数据库查询(比如基于 session ID cookie 的 session 认证),edge 帮不了忙——你仍然需要查询数据库,这意味着一次到源区域的往返。
边缘缓存#
在 edge 做缓存是事情变得有趣的地方。Edge 节点可以缓存响应,这意味着后续对同一 URL 的请求可以直接从 edge 提供,完全不需要请求源服务器。
正确使用 Cache-Control#
// app/api/products/route.ts
export const runtime = "edge";
export async function GET(request: Request) {
const url = new URL(request.url);
const category = url.searchParams.get("category") ?? "all";
const products = await fetch(
`${process.env.ORIGIN_API}/products?category=${category}`,
).then((r) => r.json());
return Response.json(products, {
headers: {
// Cache on CDN for 60 seconds
// Serve stale while revalidating for up to 5 minutes
// Client can cache for 10 seconds
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300, max-age=10",
// Vary by these headers so different variants get different cache entries
Vary: "Accept-Language, Accept-Encoding",
// CDN-specific cache tag for targeted invalidation
"Cache-Tag": `products,category-${category}`,
},
});
}stale-while-revalidate 模式在 edge 上特别强大。以下是它的工作原理:
- 第一次请求:Edge 从源服务器获取,缓存响应,返回给用户
- 60 秒内的请求:Edge 从缓存提供(0ms 源服务器延迟)
- 61-360 秒的请求:Edge 立即提供过期的缓存版本,但在后台从源服务器获取新版本
- 360 秒之后:缓存完全过期,下一个请求直接请求源服务器
你的用户几乎总是得到缓存的响应。新鲜度的权衡是显式且可调的。
Edge Config 实现动态配置#
Vercel 的 Edge Config(以及其他平台的类似服务)允许你存储键值配置,并复制到每个 edge 节点。这对于功能开关、重定向规则和 A/B 测试配置非常有用——你不需要重新部署就能更新:
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
// Edge Config reads are extremely fast (~1ms) because
// the data is replicated to every edge location
const maintenanceMode = await get<boolean>("maintenance_mode");
if (maintenanceMode) {
return NextResponse.rewrite(new URL("/maintenance", request.url));
}
// Feature flags
const features = await get<Record<string, boolean>>("feature_flags");
if (features?.["new_pricing_page"] && request.nextUrl.pathname === "/pricing") {
return NextResponse.rewrite(new URL("/pricing-v2", request.url));
}
// Dynamic redirects (update redirects without redeploy)
const redirects = await get<Array<{ from: string; to: string; permanent: boolean }>>(
"redirects",
);
if (redirects) {
const match = redirects.find((r) => r.from === request.nextUrl.pathname);
if (match) {
return NextResponse.redirect(
new URL(match.to, request.url),
match.permanent ? 308 : 307,
);
}
}
return NextResponse.next();
}这是真正的游戏规则改变者。在 Edge Config 之前,更改功能开关意味着代码修改和重新部署。现在你只需在控制面板中更新一个 JSON 值,几秒钟内就全球生效。
真实的性能计算#
让我们做真实的计算而不是营销计算。我会比较三种架构,场景是一个需要查询数据库的典型 API 端点:
场景:用户资料 API(2 次数据库查询)#
架构 A:传统区域 Node.js
User in Tokyo → Origin in Virginia: 140ms
+ DB query 1 (same region): 2ms
+ DB query 2 (same region): 2ms
+ Processing: 5ms
= Total: ~149ms
架构 B:Edge Function + HTTP 数据库
User in Tokyo → Edge in Tokyo: 5ms
+ DB query 1 (HTTP to Virginia): 145ms
+ DB query 2 (HTTP to Virginia): 145ms
+ Processing: 3ms
= Total: ~298ms ← SLOWER than regional
架构 C:Edge Function + 区域数据库(只读副本)
User in Tokyo → Edge in Tokyo: 5ms
+ DB query 1 (HTTP to Tokyo replica): 8ms
+ DB query 2 (HTTP to Tokyo replica): 8ms
+ Processing: 3ms
= Total: ~24ms ← Fastest, but requires multi-region DB
架构 D:Edge 做认证 + 区域服务器做数据
User in Tokyo → Edge middleware in Tokyo: 5ms (JWT verify)
→ Origin in Virginia: 140ms
+ DB query 1 (same region): 2ms
+ DB query 2 (same region): 2ms
+ Processing: 5ms
= Total: ~154ms
(But auth is already verified — origin doesn't need to re-verify)
(And unauthorized requests are blocked at the edge — never reach origin)
要点总结:
- Edge + 源数据库 = 通常更慢,不如直接用区域服务器
- Edge + 多区域数据库 = 最快,但最贵最复杂
- Edge 做守门 + 区域做数据 = 最佳务实平衡
- 纯 Edge(无数据库)= 重定向和认证检查的无敌方案
架构 D 是我大多数项目使用的方案。Edge 处理它擅长的事(快速决策、认证、路由),区域 Node.js 服务器处理它擅长的事(数据库查询、重计算)。
Edge 真正获胜的时候:无数据库操作#
当不涉及数据库时,计算完全反转:
Redirect (edge):
User in Tokyo → Edge in Tokyo → redirect response: ~5ms
Redirect (regional):
User in Tokyo → Origin in Virginia → redirect response: ~280ms
Static API response (edge + cache):
User in Tokyo → Edge in Tokyo → cached response: ~5ms
Static API response (regional):
User in Tokyo → Origin in Virginia → response: ~280ms
Bot blocking (edge):
Bad bot in anywhere → Edge (nearest) → 403 response: ~5ms
(Bot never reaches your origin server)
Bot blocking (regional):
Bad bot in anywhere → Origin in Virginia → 403 response: ~280ms
(Bot still consumed origin resources)
对于不需要数据库的操作,edge 快 20-50 倍。这不是营销——这是物理。
我的决策框架#
在生产环境中使用 edge functions 一年后,以下是我为每个新端点或逻辑做决策时使用的流程图:
第一步:需要 Node.js API 吗?#
如果导入了 fs、net、child_process 或任何原生模块——Node.js 区域。没有讨论余地。
第二步:需要数据库查询吗?#
如果需要,而且你的用户附近没有只读副本——Node.js 区域(与数据库同一区域)。数据库往返会主导一切。
如果需要,而且你有全球分布的只读副本——Edge 可以工作,使用基于 HTTP 的数据库客户端。
第三步:是否是关于请求的决策(路由、认证、重定向)?#
如果是——Edge。这是最佳适用场景。你在请求到达源服务器之前做出快速决策。
第四步:响应是否可缓存?#
如果是——Edge 配合适当的 Cache-Control header。即使第一次请求到达源服务器,后续请求也会从 edge 缓存提供。
第五步:是否 CPU 密集?#
如果涉及大量计算(图片处理、PDF 生成、大数据转换)——Node.js 区域。
第六步:对延迟有多敏感?#
如果是后台任务或 webhook——Node.js 区域。没人在等它。 如果是用户请求且每毫秒都重要——Edge,前提是满足其他条件。
速查表#
// ✅ PERFECT for edge
// - Middleware (auth, redirects, rewrites, headers)
// - Geolocation logic
// - A/B test assignment
// - Bot detection / WAF rules
// - Cache-friendly API responses
// - Feature flag checks
// - CORS preflight responses
// - Static data transformations (no DB)
// - Webhook signature verification
// ❌ KEEP on Node.js regional
// - Database CRUD operations
// - File uploads / processing
// - Image manipulation
// - PDF generation
// - Email sending (use HTTP API, but still regional)
// - WebSocket servers
// - Background jobs / queues
// - Anything using native npm packages
// - SSR pages with database queries
// - GraphQL resolvers that hit databases
// 🤔 IT DEPENDS
// - Authentication (edge for JWT, regional for session-DB)
// - API routes (edge if no DB, regional if DB)
// - Server-rendered pages (edge if data comes from cache/fetch, regional if DB)
// - Real-time features (edge for initial auth, regional for persistent connections)我实际在 Edge 上运行的东西#
对于这个站点,以下是具体分工:
Edge(middleware):
- 语言检测和重定向
- 机器人过滤
- 安全 header(CSP、HSTS 等)
- 访问日志
- 速率限制(基础)
Node.js 区域:
- 博客内容渲染(MDX 处理需要通过 Velite 使用 Node.js API)
- 涉及 Redis 的 API 路由
- OG 图片生成(需要更多 CPU 时间)
- RSS 订阅生成
静态(不需要运行时):
- 工具页面(构建时预渲染)
- 博客文章页面(构建时预渲染)
- 所有图片和资源(CDN 提供)
最好的运行时通常是不需要运行时。如果你能在构建时预渲染某些东西并作为静态资源提供,那永远比任何 edge function 都快。Edge 是为那些在每次请求时确实需要动态处理的东西准备的。
诚实的总结#
Edge functions 不是传统服务器的替代品,而是补充。它们是你架构工具箱中的一个额外工具——对正确的用例来说极其强大,对错误的用例来说则有害。
我不断回到的启发式规则是:如果你的函数需要访问单一区域的数据库,把函数放在 edge 上不会有帮助——反而会拖累。 你多加了一跳。函数运行得更快了,但然后它花了 100ms+ 去请求数据库。最终结果:比把所有东西放在一个区域还慢。
但对于只需要请求本身信息就能做出的决策——地理位置、cookie、header、JWT——edge 是无与伦比的。那些 5ms 的 edge 响应不是合成基准测试,它们是真实的,你的用户能感受到差异。
不要把所有东西搬到 edge。也不要把所有东西都留在 edge 之外。把每个逻辑放在物理规律最有利的地方。