切换语言
切换主题

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 哪些要缓存、缓存多久。虽然麻烦了点,但至少知道发生了什么,可控性强多了。

性能优化的三个方向

搞清楚问题后,优化方向就明确了:

  1. 缓存:不要重复做已经做过的事
  2. 流式响应:边算边传,别等全部准备好再返回
  3. 边缘计算:把服务器搬到离用户近的地方

接下来一个个讲。

缓存策略:选对方法事半功倍

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 小时,之后自动刷新。

500ms → 50ms
响应时间降低 90%

场景二:用户相关数据缓存

用户个人资料这种数据变化不频繁,但也不能一直用旧的。这时候可以用 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 提供了 revalidateTagrevalidatePath 两个 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 就能看到前几条数据,可以先浏览着”。体验完全不同。

什么时候用流式响应?

我总结了几个典型场景:

  1. 长列表数据:商品列表、文章列表、搜索结果
  2. AI 生成内容:ChatGPT 那种打字机效果,其实就是流式响应
  3. 大文件处理:导出 Excel、生成 PDF
  4. 实时日志:构建日志、任务进度

基本上,只要数据量大或计算耗时,都值得考虑流式响应。

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'
    }
  })
}

代码不复杂。核心就是:

  1. 创建 ReadableStream
  2. start 方法里分批获取数据
  3. controller.enqueue() 发送每批数据
  4. 全部发完后调用 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)
  }
}

用户打开页面后,列表会逐步显示内容,而不是长时间白屏。

实际效果对比

我给博客列表加了流式响应后:

2800ms → 500ms
首屏显示时间

虽然总时间只快了 1300ms,但用户感知快了不止一倍。因为 500ms 就能交互了,剩下的时间用户在浏览内容,根本没在等。

一个小技巧

如果数据量特别大,可以结合虚拟滚动(Virtual Scrolling)。前端只渲染可见区域的数据,其他的先不渲染。这样即使接收了 1000 条数据,页面也不会卡。

React 可以用 react-windowreact-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,比如 fscrypto、数据库连接等。

Edge Runtime 不一样,它基于 V8 引擎(Chrome 浏览器用的那个),不是完整的 Node.js 环境。好处是启动超快(0-5ms),坏处是很多 Node.js API 用不了。

简单对比一下:

特性Node.js RuntimeEdge Runtime
启动速度100-500ms0-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' })
}
200ms → 20ms
鉴权延迟降低 90%

场景二:地理位置个性化

根据用户 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

fspathchild_process 这些都用不了。如果代码里用了这些,迁移到 Edge 会报错。

限制二:数据库连接

传统的数据库连接方式(如 pgmysql2)用不了,因为它们依赖 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 }
  })
}

无效请求在边缘就被拦截了,不会打到中心服务器。

优化效果对比

指标优化前优化后提升
首次访问响应时间2800ms300ms(首批)89% ↓
缓存命中响应时间-50ms98% ↓
JSON 大小2.3MB180KB92% ↓
用户可交互时间2800ms300ms89% ↓
服务器负载100%10%90% ↓

用户再也不会吐槽”这是什么年代的网站”了。

性能监控与持续优化

优化完了不是结束,要持续监控才知道效果。

关键指标

我关注这几个指标:

  1. 响应时间分布(P50、P95、P99)

    • P50(中位数):一半用户的体验
    • P95:95% 用户的体验
    • P99:最慢 1% 用户的体验(可能反映异常情况)
  2. 缓存命中率

    • 命中率 <70% 说明缓存策略有问题
    • 命中率 >95% 可能缓存时间太长,数据不新鲜
  3. 错误率

    • 优化后要确保错误率没升高
    • 流式响应可能在中途失败,要特别注意
  4. 地理分布

    • 不同地区用户的延迟差异
    • 决定是否需要 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)
}

持续优化建议

  1. 定期审查缓存策略:业务变了,缓存策略也要调整
  2. A/B 测试:不确定哪个方案好?测试一下
  3. 根据真实数据调整:别凭感觉,看监控数据再决定

性能优化是持续的过程,不是一劳永逸的。

总结

说了这么多,总结一下核心要点:

缓存策略:根据数据特征选方案。静态数据长缓存,用户数据短缓存配合 stale-while-revalidate,实时数据别缓存。别忘了数据更新后清除缓存。

流式响应:数据量大或计算慢时的银弹。让用户早点看到内容,别盯着白屏干等。前端配合虚拟滚动效果更好。

Edge Functions:适合鉴权、地理位置判断、API 代理这种轻量级逻辑。别指望它处理复杂业务,和 Node.js Runtime 混合用才是正道。

优化不是一蹴而就的。从最慢的接口开始,应用这三招,实测效果,再调整。一步步来,别想着一次性完美。

我那个博客列表 API 从 3 秒优化到 300ms,用户体验改善特别明显。你也可以试试,选一个慢接口,今天就开始优化。有问题欢迎评论交流,咱们一起进步。

常见问题

Next.js API 缓存什么时候会失效?
有三种失效方式:1) 时间失效(revalidate 设置的时间到了),2) 手动失效(调用 revalidateTag 或 revalidatePath),3) 用户强制刷新(Ctrl+Shift+R)。最常用的是前两种,建议根据数据更新频率设置合适的 revalidate 时间。
流式响应适合所有接口吗?
不是。流式响应适合数据量大(如长列表)或计算耗时(如 AI 生成)的场景。如果数据量小且计算快,用传统响应就够了,没必要增加复杂度。判断标准:响应时间 >1 秒或 JSON 大小 >500KB 时考虑流式响应。
Edge Functions 有哪些限制?
主要三个限制:1) 不能用 Node.js 专属 API(如 fs、child_process),2) 数据库连接要用 HTTP-based 方案(如 Prisma Data Proxy),3) 内存限制 128MB 和执行时间限制 30 秒。适合鉴权、代理等轻量逻辑,复杂业务还是用 Node.js Runtime。
如何选择缓存策略?
看数据更新频率:静态数据(配置、分类)用长缓存(1 小时+),用户数据(个人资料)用 stale-while-revalidate(60 秒新鲜+300 秒后台更新),实时数据(股票价格)不缓存或用 WebSocket。记住:缓存时间越长性能越好,但数据越可能过期。
优化后如何验证效果?
关注四个指标:1) 响应时间(P50、P95、P99),2) 缓存命中率(目标 70-95%),3) 错误率(不能因优化而升高),4) 地理分布延迟。可以用 Vercel Analytics、Next.js Instrumentation API 或自定义日志监控。记得做 A/B 测试对比优化前后。

15 分钟阅读 · 发布于: 2026年1月5日 · 修改于: 2026年1月22日

评论

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

相关文章