Next.js 请求最佳实践 - vercel 2026一月发布指南

你打开商品详情页,转了 3 秒菊花才看到内容------慢在哪?

你点个"加购物车",页面卡了一下才反应------又慢在哪?

你加了几个功能,整个站越来越肥,首屏白得让人以为断网了------还是慢在哪?

大部分人的第一反应是:肯定是代码写得不够优雅,赶紧 useMemouseCallback 一把梭。

错了。

真正把你拖垮的,往往不是"组件重渲染了 2 次还是 3 次",而是两座更致命的大山:

  • 请求瀑布:本来能并行的请求,被你写成了串行,用户白等 600ms 起步
  • 客户端屎山:本来能在服务端干的活,全塞客户端,每个页面多背 300KB JS

Vercel 把这事儿说得很直白:你优化的顺序错了,再怎么抠细节都是白干。

先看两个要命的数字

数字 1:一个瀑布 = 白等 600ms

什么叫"请求瀑布"?看这段代码:

tsx 复制代码
async function loadPage() {
  const user = await fetchUser()      // 等 200ms
  const product = await fetchProduct() // 又等 200ms  
  const inventory = await fetchInventory() // 再等 200ms
}

这三个请求明明互不依赖,结果你愣是让用户等了 600ms。改成并行呢?

tsx 复制代码
const [user, product, inventory] = await Promise.all([
  fetchUser(),
  fetchProduct(), 
  fetchInventory()
])
// 总共只等 200ms

省了 400ms,比你优化一百个 useMemo 都管用。

还有更骚的操作:你的代码先 await fetchUserData(),但后面某个分支根本用不到这数据------结果不管走不走那个分支,都得先等完这个请求。白等的典范。

数字 2:每页多背 300KB = 长期税

你今天为了"方便",把数据请求、状态管理、第三方库全塞客户端。爽是爽了,代价是什么?

每个用户每次访问,都要下载这 300KB 的 JS,解析它,执行它。

这不是一次性成本,这是"长期税"------你写一次,所有用户永远买单,直到有人受不了来重构。

明确一点:Vercel 把这两件事排在所有性能优化的最前面,标注为 CRITICAL。

什么 useMemo、组件拆分、虚拟滚动------都得往后稍稍。因为你瀑布多 600ms,用户根本活不到看你"优雅的重渲染控制"。

请求选型

别管什么 RSC、Server Component、Server Action 这些名词吓人。你只需要记住:遇到场景,先问两句话。

第一问:这么干会不会让用户白等(制造瀑布)?
第二问:这么干会不会让包体越滚越大(养长期税)?

两个都不会?那才轮到你聊别的细节。

场景 1:打开页面就要数据 → 用 RSC

典型需求: 商品详情页、列表页、仪表盘

用来预防数据在客户端一层层触发,形成等待链。

tsx 复制代码
// ❌ 错误示范(客户端瀑布)
function ProductPage() {
  const [user, setUser] = useState(null)
  const [product, setProduct] = useState(null)
  
  useEffect(() => {
    fetchUser().then(setUser)  // 先等这个
  }, [])
  
  useEffect(() => {
    if (user) {
      fetchProduct(user.id).then(setProduct) // 再等这个
    }
  }, [user])
  
  return <div>...</div>
}

这代码每一行都在喊"我在制造瀑布"。

正确姿势:RSC 在服务端并行拿数据

tsx 复制代码
// ✅ app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
  // 并行发起,一起等
  const [user, product, inventory] = await Promise.all([
    getUser(),
    getProduct(params.id),
    getInventory(params.id)
  ])
  
  return <ProductDetail user={user} product={product} inventory={inventory} />
}

核心思路: 能并行就并行,别让服务端也写出瀑布。数据拿齐了再渲染,客户端收到的就是带数据的 HTML。

场景 2:用户点按钮要写数据 → 用 Server Action

典型需求: 加购物车、提交表单、点赞、删除

杜绝把一次写操作拆成多段:写完拉数据、拉完算状态、算完再渲染......

tsx 复制代码
// ❌ 错误示范(客户端拉长等待链)
async function handleAddCart() {
  await fetch('/api/cart', { method: 'POST', ... }) // 等
  const newCart = await fetch('/api/cart').then(r => r.json()) // 又等
  setCart(newCart) // 状态更新
  toast.success('已加入购物车') // 最后才提示
}

正确姿势:Server Action 一把梭

tsx 复制代码
// ✅ app/products/actions.ts
'use server'

export async function addToCart(productId: string) {
  const user = await getCurrentUser()
  
  // 并行:写库 + 查库存
  const [cart, stock] = await Promise.all([
    db.cart.create({ userId: user.id, productId }),
    db.inventory.findUnique({ where: { productId } })
  ])
  
  revalidatePath('/cart')  // 刷新相关页面
  return { success: true, stock: stock.quantity }
}
tsx 复制代码
// ✅ Client Component 里直接调
'use client'
import { addToCart } from './actions'

function AddToCartButton({ productId }) {
  return (
    <button onClick={async () => {
      const result = await addToCart(productId)
      toast.success(`已加入!剩余 ${result.stock} 件`)
    }}>
      加入购物车
    </button>
  )
}

核心思路: Server Action 就是"组件内的服务端入口"。该并行并行,写完直接 revalidatePath 刷新,别让客户端再发一圈请求。

场景 3:外部系统要打你 → 用 Route Handler (app/api)

典型需求: Stripe webhook、GitHub 回调、给 App 提供 API

为什么不能用 Action? 因为外部系统不认你的组件树,它只认一个 HTTP URL。

tsx 复制代码
// ✅ app/api/webhooks/stripe/route.ts
export async function POST(req: Request) {
  const signature = req.headers.get('stripe-signature')
  
  // 验签
  const event = stripe.webhooks.constructEvent(
    await req.text(),
    signature,
    process.env.STRIPE_WEBHOOK_SECRET
  )
  
  // 并行:写库 + 触发发货
  await Promise.all([
    db.order.update({ where: { id: event.data.object.metadata.orderId }, data: { status: 'paid' } }),
    triggerShipment(event.data.object.metadata.orderId)
  ])
  
  return Response.json({ received: true })
}

核心思路: Route Handler = 对外的 HTTP 边界。但内部逻辑照样要砍瀑布、控包体。

最容易踩的坑:Server Action 里绕圈打自己的 API

很多人会写成这样:

tsx 复制代码
// ❌ 多此一举
'use server'
export async function createOrder(data) {
  const result = await fetch('http://localhost:3000/api/orders', {
    method: 'POST',
    body: JSON.stringify(data)
  })
  return result.json()
}

你人已经在服务端了,为什么还要绕 HTTP 一圈?

这么干的后果:

  • 多一跳网络请求(更容易瀑布)
  • 多一次序列化/反序列化
  • 多一层错误处理

正确姿势:直接调业务逻辑

tsx 复制代码
// ✅ server/order.service.ts
export async function createOrder(data, userId) {
  // 校验、写库、触发通知...
  return db.order.create({ data: { ...data, userId } })
}

// ✅ app/orders/actions.ts
'use server'
import { createOrder } from '@/server/order.service'

export async function createOrderAction(formData) {
  const user = await getCurrentUser()
  return createOrder(parseFormData(formData), user.id)
}

什么时候才该用 /api?

  • 外部系统要调(webhook、移动端)
  • 需要自定义 Response(下载文件、streaming)
  • 需要标准 HTTP 语义(状态码、特殊 header)

内部写操作?直接 Action 调 Service,别绕。

速查表:一张图看完怎么选

场景 用什么 核心原则
打开页面要数据 RSC (Server Component) 并行拿数据,别串行等
页面内写数据 Server Action 别拉长等待链,别推客户端
外部系统回调 Route Handler (/api) 对外边界,内部逻辑照样砍瀑布
交互需要状态 Client Component 只放必须的,别啥都塞客户端

拿不准?回到两句话:

  1. 会不会多等 600ms?(瀑布)
  2. 会不会多背 300KB?(包体税)

实战目录长这样

bash 复制代码
app/
├── products/
│   ├── page.tsx           # RSC:并行拿数据渲染
│   ├── [id]/page.tsx      # RSC:详情页
│   └── actions.ts         # Server Actions:写操作
│
├── api/
│   └── webhooks/
│       └── stripe/
│           └── route.ts   # Route Handler:外部回调
│
server/                    # 业务逻辑层(Action 和 API 都调这里)
├── product.service.ts     # 组合逻辑、权限、事务
└── product.repo.ts        # 纯数据访问(DB/外部 API)

components/                # UI 组件
├── ProductCard.tsx        # Server Component(默认)
└── AddToCartButton.tsx    # Client Component("use client")

核心思路:

  • app/ 负责路由和 UI 组合
  • server/ 负责业务逻辑(可复用)
  • components/ 负责展示和交互

Action 和 Route Handler 都不写业务细节,都调 server/ 里的函数。这样:

  • 逻辑不重复
  • 测试更好写
  • 重构不伤筋动骨

最后

架构不是"我用了多少高级名词",是**"我让用户少等了多少时间"**。

先砍 600ms 的瀑布,再砍 300KB 的包体税。剩下的 useMemo、memo、虚拟滚动------等你把这两座大山移平了再说。

记住:能并行就并行,能服务端就服务端。

别等页面慢得用户骂娘了,才想起来"哦对,我好像写了个瀑布"。

参考:

  1. raw.githubusercontent.com/vercel-labs...
  2. github.com/vercel-labs...
相关推荐
ccnocare1 小时前
浅浅看一下设计模式
前端
Lee川1 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix2 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人2 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl2 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人2 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼2 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空2 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust