Next.js API 性能优化完全指南:缓存策略、流式响应与边缘计算实战

周五晚上九点,产品经理在群里发了个截屏。用户测试视频里,测试员在手机上点开博客列表页,Loading 图标转了整整 5 秒,页面还是白的。右下角有句话:“这是什么年代的网站?”
我打开 Chrome DevTools,好家伙,API 请求耗时 3200ms。说实话那一刻我有点慌,虽然知道这个接口慢,但一直没空优化,没想到慢成这样。
后来花了两天研究 Next.js 的性能优化,发现其实没那么复杂。用对缓存策略,加上流式响应和边缘计算,响应时间直接从 3 秒降到 500ms 以内。更重要的是,我搞清楚了什么时候用哪种方案——这比知道有什么技术重要得多。
今天就来聊聊这三招:缓存策略怎么选、流式响应怎么做、Edge Functions 什么场景适合用。代码都是真实跑过的,性能数据也是实测的,拿去就能用。
为什么你的 Next.js API 这么慢?
先说说常见的性能瓶颈。我当时排查那个 3 秒的接口,发现了几个典型问题:
数据库查询没优化。代码里有个循环,每条文章都单独查一次作者信息——经典的 N+1 查询。100 篇文章就是 100 次数据库请求,能不慢吗?更坑的是,有些表连索引都没建。
完全没有缓存。每次用户刷新页面,服务器都重新查数据库、重新计算、重新格式化。明明配置信息一个月才变一次,却每秒都在重复计算。
一口气返回所有数据。接口一次返回 100 篇文章的完整内容,包括文章正文。JSON 响应 2MB 多,网络传输就要 1 秒。其实列表页根本不需要正文,只要标题和摘要。
服务器地理位置。我们把服务器部署在美国西海岸,国内用户访问一个来回就是 200ms 起步,再加上 GFW 的影响…不谈了。
Next.js 16 带来的缓存变化
2025 年 10 月,Next.js 16 发布了一个挺重要的更新:从隐式缓存变成了显式缓存。
以前 Next.js 会自动帮你缓存很多东西,听起来很方便,但实际用起来经常懵:这玩意到底缓存了没?缓存多久?怎么清除?很多时候数据明明更新了,页面还显示旧的,调试半天才发现是缓存的锅。
现在你得明确告诉 Next.js 哪些要缓存、缓存多久。虽然麻烦了点,但至少知道发生了什么,可控性强多了。
性能优化的三个方向
搞清楚问题后,优化方向就明确了:
- 缓存:不要重复做已经做过的事
- 流式响应:边算边传,别等全部准备好再返回
- 边缘计算:把服务器搬到离用户近的地方
接下来一个个讲。
缓存策略:选对方法事半功倍
Next.js 的缓存机制有四种:Request Memoization、Data Cache、Full Route Cache、Router Cache。第一次看文档时我也懵,这么多类型怎么记?
其实不用都记。对 API Routes 来说,最常用的就是 Data Cache——把数据库查询结果或外部 API 的响应缓存起来。
场景一:静态数据缓存
比如网站配置、分类列表这种几乎不变的数据,完全可以缓存个把小时。
// app/api/categories/route.js
export async function GET() {
const data = await fetch('https://api.example.com/categories', {
next: { revalidate: 3600 } // 缓存 1 小时
})
return Response.json(await data.json())
}就这么简单。revalidate: 3600 表示缓存 1 小时,之后自动刷新。
场景二:用户相关数据缓存
用户个人资料这种数据变化不频繁,但也不能一直用旧的。这时候可以用 stale-while-revalidate 策略:
// app/api/user/profile/route.js
export async function GET(request) {
const user = await getUserFromDB()
return new Response(JSON.stringify(user), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 's-maxage=60, stale-while-revalidate=300'
}
})
}这个策略很聪明:先返回缓存的数据(即使可能过期了),同时后台异步更新缓存。用户感知不到延迟,但数据也不会太旧。
s-maxage=60 表示缓存 60 秒内是新鲜的,stale-while-revalidate=300 表示过期后 300 秒内可以继续用旧数据,同时后台更新。
场景三:实时数据不要缓存
股票价格、聊天消息这种实时性要求高的数据,就别缓存了。要么完全不缓存,要么用 WebSocket 或 Server-Sent Events 推送。
export async function GET() {
const price = await getStockPrice()
return new Response(JSON.stringify(price), {
headers: {
'Cache-Control': 'no-store' // 不缓存
}
})
}缓存失效:数据更新后怎么办?
用户更新了个人资料,缓存还显示旧的?这时候需要手动清除缓存。
Next.js 提供了 revalidateTag 和 revalidatePath 两个 API:
// app/api/user/update/route.js
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const data = await request.json()
await updateUserProfile(data)
// 清除用户相关的缓存
revalidateTag('user-profile')
return Response.json({ success: true })
}对应的,在查询接口里给缓存打上标签:
export async function GET() {
const data = await fetch('db-api/user', {
next: {
revalidate: 3600,
tags: ['user-profile'] // 标签
}
})
return Response.json(await data.json())
}这样更新资料后,相关缓存立即失效,下次请求就会拿到最新数据。
常见坑
坑一:过度缓存。我见过有人把订单状态缓存 1 小时,结果用户付款后半天看不到订单状态更新。缓存时间要根据数据特征来定,不是越长越好。
坑二:忘记缓存预热。首次请求依然很慢,因为缓存是空的。可以在部署后主动调用一次接口,把热点数据提前加载进缓存。
坑三:缓存 key 设计不当。比如用户 A 的数据被缓存了,结果用户 B 也拿到了 A 的数据。要确保缓存 key 包含用户 ID 等区分信息。
流式响应:大数据传输不再卡顿
缓存解决了重复计算的问题,但有些数据就是算得慢、数据量大。这时候流式响应就派上用场了。
流式响应是什么?
传统的 API 响应就像去餐厅吃饭:厨师把所有菜都做好了才一起端上桌。如果点了 10 道菜,就得等最慢的那道做完。
流式响应不一样:做好一道上一道,客人边吃边等下一道。虽然总时间可能差不多,但客人早就开始吃了,不用饿着干等。
对用户来说,就是从”盯着白屏等 3 秒”变成”500ms 就能看到前几条数据,可以先浏览着”。体验完全不同。
什么时候用流式响应?
我总结了几个典型场景:
- 长列表数据:商品列表、文章列表、搜索结果
- AI 生成内容:ChatGPT 那种打字机效果,其实就是流式响应
- 大文件处理:导出 Excel、生成 PDF
- 实时日志:构建日志、任务进度
基本上,只要数据量大或计算耗时,都值得考虑流式响应。
Next.js 里怎么实现?
最常用的方式是 ReadableStream:
// app/api/posts/stream/route.js
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
// 分批获取数据
for (let page = 0; page < 5; page++) {
// 每次查 20 条
const posts = await fetchPostsFromDB({ page, limit: 20 })
// 发送这批数据
const chunk = JSON.stringify(posts) + '\n'
controller.enqueue(encoder.encode(chunk))
// 模拟处理时间
await new Promise(r => setTimeout(r, 100))
}
// 数据发送完毕
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Transfer-Encoding': 'chunked'
}
})
}代码不复杂。核心就是:
- 创建
ReadableStream - 在
start方法里分批获取数据 - 用
controller.enqueue()发送每批数据 - 全部发完后调用
controller.close()
前端怎么接收?
后端发了流式数据,前端也要相应处理:
async function fetchStreamData() {
const response = await fetch('/api/posts/stream')
const reader = response.body.getReader()
const decoder = new TextDecoder()
let allPosts = []
while (true) {
const { done, value } = await reader.read()
if (done) {
console.log('数据接收完毕')
break
}
// 解码数据
const chunk = decoder.decode(value)
// 解析 JSON(每行一个)
const posts = JSON.parse(chunk)
allPosts = [...allPosts, ...posts]
// 实时更新 UI
updatePostList(allPosts)
}
}用户打开页面后,列表会逐步显示内容,而不是长时间白屏。
实际效果对比
我给博客列表加了流式响应后:
虽然总时间只快了 1300ms,但用户感知快了不止一倍。因为 500ms 就能交互了,剩下的时间用户在浏览内容,根本没在等。
一个小技巧
如果数据量特别大,可以结合虚拟滚动(Virtual Scrolling)。前端只渲染可见区域的数据,其他的先不渲染。这样即使接收了 1000 条数据,页面也不会卡。
React 可以用 react-window 或 react-virtualized 库,Vue 可以用 vue-virtual-scroller。
Edge Functions:把 API 搬到用户家门口
缓存和流式响应都是软件层面的优化,但还有个更直接的方法:把服务器搬到离用户近的地方。
物理距离的影响
网络请求的延迟主要来自物理距离。光速是有限的,数据包从北京到美国西海岸,一个来回至少 200ms,这是物理规律,没法优化。
以前我们只能把服务器部署在固定地点,比如阿里云北京。北京用户访问快,但美国用户访问就慢了。
Edge Functions 的思路很简单:把代码部署到全球几十个甚至上百个节点,用户访问时自动路由到最近的节点。北京用户访问北京节点,纽约用户访问纽约节点,延迟能降到 50ms 以内。
Edge Runtime 和 Node.js Runtime 的区别
Next.js 的 API Routes 默认跑在 Node.js Runtime 上,可以用所有 Node.js API,比如 fs、crypto、数据库连接等。
Edge Runtime 不一样,它基于 V8 引擎(Chrome 浏览器用的那个),不是完整的 Node.js 环境。好处是启动超快(0-5ms),坏处是很多 Node.js API 用不了。
简单对比一下:
| 特性 | Node.js Runtime | Edge Runtime |
|---|---|---|
| 启动速度 | 100-500ms | 0-5ms |
| 可用 API | 全部 Node.js API | 受限(仅 Web 标准 API) |
| 适用场景 | 复杂业务逻辑、数据库操作 | 轻量逻辑、鉴权、代理 |
| 全球延迟 | 取决于部署位置 | 全球 <50ms |
| 内存限制 | 较高 | 较低(128MB) |
什么场景适合用 Edge Functions?
不是所有 API 都适合迁移到 Edge。我总结了几个典型场景:
场景一:身份鉴权
最适合 Edge 的场景就是鉴权。检查 JWT token、验证 API key 这种轻量级逻辑,在边缘完成就行,无效请求根本不用到达中心服务器。
// app/api/auth/route.js
export const runtime = 'edge'
export async function GET(request) {
const token = request.headers.get('authorization')
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
// 验证 token(可以用 jose 库,支持 Edge)
const isValid = await verifyToken(token)
if (!isValid) {
return new Response('Invalid token', { status: 401 })
}
return Response.json({ user: 'authenticated' })
}场景二:地理位置个性化
根据用户 IP 返回不同内容,比如不同语言、货币、推荐内容。
export const runtime = 'edge'
export async function GET(request) {
// 获取用户地理位置(Vercel 会自动注入)
const country = request.geo?.country || 'US'
const city = request.geo?.city || 'Unknown'
// 根据位置返回不同内容
const content = getLocalizedContent(country)
return Response.json({
country,
city,
content,
currency: country === 'CN' ? 'CNY' : 'USD'
})
}不需要查数据库,边缘直接处理,超快。
场景三:API 代理
有时候前端需要调用多个外部 API,可以在 Edge 层做聚合,减少客户端请求次数。
export const runtime = 'edge'
export async function GET(request) {
// 并行请求多个 API
const [weather, news] = await Promise.all([
fetch('https://api.weather.com/...'),
fetch('https://api.news.com/...')
])
return Response.json({
weather: await weather.json(),
news: await news.json()
})
}用户只发一个请求,后端并行处理,总延迟大幅降低。
场景四:A/B 测试
在边缘层决定返回哪个版本的内容,不需要改动主应用。
export const runtime = 'edge'
export async function GET(request) {
const userId = request.headers.get('x-user-id')
// 简单的 A/B 分流逻辑
const variant = parseInt(userId) % 2 === 0 ? 'A' : 'B'
const content = variant === 'A' ? getContentA() : getContentB()
return Response.json({ variant, content })
}Edge Functions 的限制
Edge 这么好,为啥不全部迁移?因为限制挺多的:
限制一:不能用 Node.js 专属 API
fs、path、child_process 这些都用不了。如果代码里用了这些,迁移到 Edge 会报错。
限制二:数据库连接
传统的数据库连接方式(如 pg、mysql2)用不了,因为它们依赖 Node.js 的 net 模块。要用 HTTP-based 的方案,比如:
- Prisma Data Proxy
- PlanetScale(MySQL)
- Supabase(PostgreSQL)
- Redis(支持 HTTP API)
限制三:内存和执行时间限制
Edge Functions 通常有内存限制(128MB)和执行时间限制(30 秒)。复杂计算或大数据处理不适合。
我的建议:混合使用
不用非此即彼。我的做法是:
- 边缘层(Edge):鉴权、地理位置判断、简单代理
- 中心层(Node.js):复杂业务逻辑、数据库操作、文件处理
Edge 层挡住无效请求和简单请求,复杂的再转发给中心服务器。这样既降低延迟,又不受 Edge 限制。
性能实测
根据 Medium 上的一篇 benchmark 研究:
- Vercel Edge Functions:平均延迟 48.3ms
- Cloudflare Workers(自定义):平均延迟 36.37ms
- 传统 Node.js API(单地区):平均延迟 200-500ms
Edge 确实快,但具体效果还得看你的用户分布。如果用户都在国内,部署在国内的传统服务器可能更快。
综合实战案例:博客文章列表 API 优化
前面讲了三个技术,现在结合起来看看实际怎么用。就拿开头那个慢到让用户吐槽的博客列表 API 来说。
优化前的问题
先看看原来的代码:
// app/api/posts/route.js
export async function GET() {
// 问题1:每次都查数据库,无缓存
const posts = await db.post.findMany({
take: 100,
include: {
author: true, // 问题2:N+1 查询
tags: true
}
})
// 问题3:返回完整文章内容,数据量大
return Response.json(posts)
}性能数据:
- 响应时间:2800ms
- JSON 大小:2.3MB
- 用户体验:白屏 3 秒
第一步:优化数据库查询
先解决 N+1 查询问题,只返回必要字段:
export async function GET() {
const posts = await db.post.findMany({
take: 100,
select: {
id: true,
title: true,
summary: true, // 只要摘要,不要全文
createdAt: true,
author: {
select: { name: true, avatar: true }
}
}
})
return Response.json(posts)
}效果:响应时间降到 800ms,JSON 从 2.3MB 降到 180KB。
第二步:加缓存
文章列表变化不频繁,可以缓存 5 分钟:
export async function GET() {
const posts = await db.post.findMany({
// ... 同上
}, {
next: {
revalidate: 300, // 缓存 5 分钟
tags: ['posts']
}
})
return Response.json(posts)
}配合文章发布时清除缓存:
// app/api/posts/publish/route.js
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const newPost = await request.json()
await db.post.create({ data: newPost })
// 清除文章列表缓存
revalidateTag('posts')
return Response.json({ success: true })
}效果:缓存命中时响应时间 50ms,服务器负载降低 90%。
第三步:改用流式响应
虽然已经快多了,但首次访问(缓存未命中)还是要等 800ms。改成流式响应:
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const batchSize = 20
for (let page = 0; page < 5; page++) {
const posts = await db.post.findMany({
skip: page * batchSize,
take: batchSize,
select: { /* 同上 */ }
})
const chunk = JSON.stringify(posts) + '\n'
controller.enqueue(encoder.encode(chunk))
}
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'application/x-ndjson', // Newline Delimited JSON
'Cache-Control': 's-maxage=300, stale-while-revalidate=600'
}
})
}效果:首批数据 300ms 返回,用户可以立即浏览,总耗时 800ms 但用户无感知。
第四步:Edge 层鉴权(可选)
如果需要鉴权,可以在 Edge 层做初步验证:
// app/api/posts/route.js (Edge 鉴权层)
export const runtime = 'edge'
export async function GET(request) {
const token = request.headers.get('authorization')
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
// 验证通过,转发到实际 API(Node.js Runtime)
return fetch(`${process.env.API_BASE_URL}/posts/internal`, {
headers: { authorization: token }
})
}无效请求在边缘就被拦截了,不会打到中心服务器。
优化效果对比
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 首次访问响应时间 | 2800ms | 300ms(首批) | 89% ↓ |
| 缓存命中响应时间 | - | 50ms | 98% ↓ |
| JSON 大小 | 2.3MB | 180KB | 92% ↓ |
| 用户可交互时间 | 2800ms | 300ms | 89% ↓ |
| 服务器负载 | 100% | 10% | 90% ↓ |
用户再也不会吐槽”这是什么年代的网站”了。
性能监控与持续优化
优化完了不是结束,要持续监控才知道效果。
关键指标
我关注这几个指标:
响应时间分布(P50、P95、P99)
- P50(中位数):一半用户的体验
- P95:95% 用户的体验
- P99:最慢 1% 用户的体验(可能反映异常情况)
缓存命中率
- 命中率 <70% 说明缓存策略有问题
- 命中率 >95% 可能缓存时间太长,数据不新鲜
错误率
- 优化后要确保错误率没升高
- 流式响应可能在中途失败,要特别注意
地理分布
- 不同地区用户的延迟差异
- 决定是否需要 Edge Functions
监控工具
Vercel Analytics:如果用 Vercel 部署,自带性能监控,可以看到每个 API 的响应时间分布。
Next.js Instrumentation API(2026 新特性):可以在代码里插入监控点:
// instrumentation.js
export function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
require('./monitoring')
}
}
// monitoring.js
export function onRequestEnd(info) {
console.log(`API ${info.url} took ${info.duration}ms`)
// 发送到监控平台
sendToMonitoring({
url: info.url,
duration: info.duration,
status: info.status
})
}自定义日志:简单粗暴但有效:
export async function GET() {
const start = Date.now()
const data = await fetchData()
const duration = Date.now() - start
console.log(`API /posts took ${duration}ms`)
return Response.json(data)
}持续优化建议
- 定期审查缓存策略:业务变了,缓存策略也要调整
- A/B 测试:不确定哪个方案好?测试一下
- 根据真实数据调整:别凭感觉,看监控数据再决定
性能优化是持续的过程,不是一劳永逸的。
总结
说了这么多,总结一下核心要点:
缓存策略:根据数据特征选方案。静态数据长缓存,用户数据短缓存配合 stale-while-revalidate,实时数据别缓存。别忘了数据更新后清除缓存。
流式响应:数据量大或计算慢时的银弹。让用户早点看到内容,别盯着白屏干等。前端配合虚拟滚动效果更好。
Edge Functions:适合鉴权、地理位置判断、API 代理这种轻量级逻辑。别指望它处理复杂业务,和 Node.js Runtime 混合用才是正道。
优化不是一蹴而就的。从最慢的接口开始,应用这三招,实测效果,再调整。一步步来,别想着一次性完美。
我那个博客列表 API 从 3 秒优化到 300ms,用户体验改善特别明显。你也可以试试,选一个慢接口,今天就开始优化。有问题欢迎评论交流,咱们一起进步。
常见问题
Next.js API 缓存什么时候会失效?
流式响应适合所有接口吗?
Edge Functions 有哪些限制?
如何选择缓存策略?
优化后如何验证效果?
15 分钟阅读 · 发布于: 2026年1月5日 · 修改于: 2026年1月22日
相关文章
Next.js 电商实战:购物车与 Stripe 支付完整实现指南

Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战

Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南


评论
使用 GitHub 账号登录后即可评论