Next.js 15 新特性全解与升级指南
第26章:Next.js 15 新特性全解与升级指南
Next.js 15 不是简单的版本迭代——async cookies/headers、fetch 缓存默认行为反转、React 19 集成——这是过去几年最重要的一次版本发布。
本章核心问题:哪些破坏性变更必须立刻处理?React 19 带来了哪些新特性?Pages Router 如何渐进式迁移到 App Router?
读完本章你将理解:
- cookies()/headers()/params 改为异步的原因与迁移方法
- fetch 缓存默认行为从 force-cache 改为 no-store 的影响
- after() API、
Level 1 · 你需要知道的(1-3年经验)
从 14 到 15:一次有破坏性的进化
Next.js 15 不是简单的版本迭代,它包含了几个真正的破坏性变更(Breaking Changes),升级前必须理解。与此同时,它带来了 React 19 集成、Turbopack 正式稳定、新的缓存默认行为、多个实用 API——这是过去几年 Next.js 最重要的一次版本发布。
本章把变更分为两类:你必须立刻处理的破坏性变更,以及你可以按需采用的新特性。
破坏性变更一:async cookies() 和 headers()
这是升级时最常遇到的问题。在 Next.js 14 及之前,cookies() 和 headers() 是同步函数:
// Next.js 14 写法(在 Next.js 15 中会有弃用警告)
import { cookies, headers } from 'next/headers'
export default function Page() {
const cookieStore = cookies() // 同步
const token = cookieStore.get('token')
// ...
}
在 Next.js 15 中,这两个函数改为返回 Promise:
// Next.js 15 写法
import { cookies, headers } from 'next/headers'
export default async function Page() {
const cookieStore = await cookies() // 必须 await
const token = cookieStore.get('token')
const headersList = await headers()
const userAgent = headersList.get('user-agent')
}
为什么要改?这是为了支持 Next.js 的部分预渲染(Partial Prerendering)架构。同步访问请求时数据会阻止静态优化;将这些函数改为异步,Next.js 可以在渲染树中精确标记哪些部分依赖请求时数据,更细粒度地优化。
快速迁移:Next.js 提供了 codemod 工具:
npx @next/codemod@canary next-async-request-api .
这个 codemod 会自动在 Server Component 中将同步调用改为 await 调用。但涉及 Client Component 或复杂场景时需要手动检查。
破坏性变更二:fetch 缓存默认行为反转
这是 Next.js 15 最重要的语义变化,也是最容易产生 bug 的地方:
// Next.js 14:fetch 默认缓存(等同于 cache: 'force-cache')
fetch('https://api.example.com/data')
// 等价于
fetch('https://api.example.com/data', { cache: 'force-cache' })
// Next.js 15:fetch 默认不缓存(等同于 cache: 'no-store')
fetch('https://api.example.com/data')
// 等价于
fetch('https://api.example.com/data', { cache: 'no-store' })
为什么要改?Next.js 14 的默认缓存策略让很多开发者困惑——他们在页面上看到了过期数据,却不明白为什么。"默认缓存"在某些场景很高效,但它违反了最小惊奇原则(Principle of Least Surprise)。Next.js 15 选择了更符合 Web 标准的默认行为:no-store 意味着数据每次请求都是新鲜的,需要缓存的地方显式声明。
迁移影响:如果你的应用依赖 Next.js 14 的隐式缓存(这很常见),升级后可能出现:
- 页面请求变慢(原来被缓存的请求现在每次都发出)
- 服务器/数据库负载增加
- 原本缓存的内容现在实时获取(这可能是你想要的,也可能不是)
解决方案:审查所有 fetch 调用,根据需要显式添加缓存策略:
// 需要缓存的请求:显式声明
fetch('/api/config', { cache: 'force-cache' })
// 需要定时刷新的请求:使用 revalidate
fetch('/api/posts', { next: { revalidate: 3600 } }) // 1 小时
// 路由级别的缓存控制
export const revalidate = 60 // 整个路由 60 秒重新验证
// 完全不缓存
fetch('/api/live-data') // Next.js 15 默认行为,无需额外声明
Level 2 · 它是怎么运行的(3-5年经验)
React 19 集成
Next.js 15 集成了 React 19,带来了一系列新特性:
use() Hook
use() 是 React 19 的新 hook,允许在渲染过程中"解包" Promise 和 Context:
// 在 Client Component 中读取服务端传来的 Promise
'use client'
import { use } from 'react'
interface Props {
dataPromise: Promise<{ name: string }>
}
export function UserCard({ dataPromise }: Props) {
// use() 会暂停渲染直到 Promise resolve,配合 Suspense 使用
const data = use(dataPromise)
return <div>{data.name}</div>
}
// Server Component 传递 Promise 给 Client Component
export default async function Page() {
// 不 await,直接传 Promise
const dataPromise = fetchUser()
return (
<Suspense fallback={<div>加载中...</div>}>
<UserCard dataPromise={dataPromise} />
</Suspense>
)
}
这个模式的优势是:服务端可以并行发起请求,客户端的 Suspense 边界在数据就绪时自动渲染,无需手动管理 loading 状态。
Server Actions 的改进
React 19 对 Server Actions 的错误处理做了改进,useActionState(之前是 useFormState)现在有更好的类型支持:
'use client'
import { useActionState } from 'react'
import { submitForm } from './actions'
export function Form() {
const [state, action, isPending] = useActionState(submitForm, {
error: null,
success: false,
})
return (
<form action={action}>
{state.error && <p className="text-red-500">{state.error}</p>}
{state.success && <p className="text-green-500">提交成功!</p>}
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '提交'}
</button>
</form>
)
}
useOptimistic 正式稳定
React 19 使 useOptimistic 正式稳定,用于乐观更新——在服务端确认之前先在 UI 上反映变更:
'use client'
import { useOptimistic, useTransition } from 'react'
import { toggleLike } from './actions'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(current, delta: number) => current + delta
)
const [isPending, startTransition] = useTransition()
function handleLike() {
startTransition(async () => {
addOptimisticLike(1) // 立刻更新 UI
await toggleLike(postId) // 后台同步服务端
})
}
return (
<button onClick={handleLike} disabled={isPending}>
♥ {optimisticLikes}
</button>
)
}
Turbopack:开发构建正式稳定
# Next.js 15 中 next dev 默认使用 Turbopack
next dev --turbopack
# 或者简写(Next.js 15 的默认行为)
next dev
Turbopack 是用 Rust 编写的 Next.js 打包工具,在 Next.js 15 中 next dev 阶段已经正式稳定。官方基准测试显示:
- 本地服务启动:快 76%
- 代码更新(Fast Refresh):快 96%
- 大型项目效果更显著
重要限制:next build 仍然使用 webpack。Turbopack 的生产构建支持在 Next.js 15 发布时尚未稳定(预计在后续版本)。因此,开发和生产使用不同的打包器,理论上可能有细微差异,虽然实践中极少遇到。
<Form> 组件
Next.js 15 新增了 <Form> 组件,对原生 HTML <form> 的增强:
import Form from 'next/form'
export default function SearchPage() {
return (
<Form action="/search">
<input name="q" placeholder="搜索..." />
<button type="submit">搜索</button>
</Form>
)
}
<Form> 与普通 <form> 的区别:
- 客户端导航:提交时使用 Next.js 客户端导航(不是全页刷新),URL 和页面内容都更新
- 预取:当表单进入视口时,自动预取目标路由
- Loading 状态:与
useFormStatus原生集成
适合搜索框、筛选表单等"导航型"表单(提交后改变 URL 的场景)。
Level 3 · 规范怎么定义的(资深)
after() API
after() 允许在响应已经发送给客户端之后执行代码,不阻塞响应:
import { after } from 'next/server'
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
const data = await fetchData()
// 响应立刻发送,after 中的代码在响应完成后异步执行
after(async () => {
// 记录访问日志(不影响响应速度)
await prisma.accessLog.create({
data: {
path: request.nextUrl.pathname,
timestamp: new Date(),
userAgent: request.headers.get('user-agent'),
},
})
})
return NextResponse.json(data)
}
after() 的典型使用场景:
- 访问日志记录
- 分析事件上报
- 发送欢迎邮件(注册后)
- 更新缓存
关键特性:after() 中的错误不会影响已发送的响应;在 Vercel 上,after() 中的任务会在响应完成后继续运行(不受函数超时限制)。
instrumentation.ts 改进
instrumentation.ts 是 Next.js 的可观测性钩子文件,在 Next.js 15 中支持更多生命周期:
// instrumentation.ts
import { registerOTel } from '@vercel/otel'
export function register() {
// 应用启动时执行一次(服务端)
registerOTel({ serviceName: 'my-nextjs-app' })
console.log('Application started, instrumentation registered')
}
export async function onRequestError(
error: Error,
request: { path: string; method: string },
context: { routeType: string }
) {
// 捕获所有未处理的请求错误(Next.js 15 新增)
await sendToErrorTracker({
error,
path: request.path,
routeType: context.routeType,
})
}
async function sendToErrorTracker(data: object) {
// 发送到 Sentry、Datadog 等错误追踪服务
await fetch(process.env.ERROR_TRACKER_ENDPOINT!, {
method: 'POST',
body: JSON.stringify(data),
})
}
onRequestError 是 Next.js 15 的新钩子,统一捕获 Server Components、Server Actions 和 Route Handlers 中的未处理错误,是接入 Sentry 等错误监控的官方推荐位置。
Pages Router 到 App Router 的迁移策略
Next.js 支持 Pages Router 和 App Router 共存,这是渐进式迁移的基础:
app/
layout.tsx # App Router
dashboard/
page.tsx # App Router 页面
pages/
index.tsx # Pages Router 仍然工作
about.tsx # Pages Router 仍然工作
渐进式迁移策略
第一步:新功能在 App Router 中开发
不要试图一次性迁移所有路由。从新功能开始用 App Router,旧路由保持 Pages Router 运行。
第二步:迁移布局层
// app/layout.tsx — 替代 pages/_app.tsx 和 pages/_document.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>
{/* 全局 Provider、导航栏等 */}
{children}
</body>
</html>
)
}
第三步:逐路由迁移
按重要性或独立性排序,逐个迁移路由。优先迁移:不依赖其他 Pages Router 路由的页面、简单的静态页面、新增功能页面。
常见迁移陷阱
陷阱一:getServerSideProps / getStaticProps 在 App Router 中不存在
// Pages Router
export async function getServerSideProps() {
const data = await fetchData()
return { props: { data } }
}
// App Router 等价写法(直接在 async Server Component 中获取)
export default async function Page() {
const data = await fetchData()
return <div>{JSON.stringify(data)}</div>
}
陷阱二:useRouter 行为不同
// Pages Router
import { useRouter } from 'next/router' // 来自 next/router
// App Router
import { useRouter } from 'next/navigation' // 来自 next/navigation
// 注意:API 不完全相同,useRouter 在 App Router 中功能更有限
陷阱三:中间件(Middleware)是共享的
Middleware 在 Pages Router 和 App Router 请求上都会执行,确保你的 matcher 配置正确。
陷阱四:Server Components 不能使用 Context
React Context 在 Server Components 中不可用。需要在 Server Component 和 Client Component 之间共享状态,通过 props 传递或在 Client Component 边界以下使用 Context。
Next.js 14 → 15 破坏性变更检查清单
升级前逐项确认:
必须处理:
-
cookies()、headers()、draftMode()— 所有调用改为await -
fetch缓存 — 审查所有fetch调用,显式添加需要的cache选项 -
params和searchParams— 在 Pages 和 Layouts 中现在是 Promise,需要await
// Next.js 15:params 也是异步的
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ id: string }>
searchParams: Promise<{ q: string }>
}) {
const { id } = await params
const { q } = await searchParams
// ...
}
建议处理:
- React 19 兼容性 — 检查依赖库的 React 19 支持情况
-
useFormState改名为useActionState— 旧名称仍可用但已弃用 - Turbopack — 测试开发体验,报告不兼容的 webpack 插件
可选采用:
-
after()API — 将日志/分析代码迁移到after() -
<Form>组件 — 替换搜索和筛选表单 -
instrumentation.ts的onRequestError— 接入错误监控
执行升级:
npx @next/upgrade
这个命令会自动将 next、react、react-dom 升级到最新版本,并运行推荐的 codemod。
Next.js 15 的核心信号是清晰的:向标准 Web API 靠拢(异步请求 API、符合标准的 fetch 缓存语义)、拥抱 React 19 的并发特性(use()、useOptimistic)、在工具链上投入(Turbopack)。这些变化短期需要迁移成本,长期是正确的方向。
Level 4 · 边界与陷阱(所有人)
陷阱1:params 和 searchParams 在 Next.js 15 中也是 Promise——这是除了 cookies/headers 之外最常遗漏的异步迁移点。
陷阱2:useFormState 已改名为 useActionState——旧名称仍可用但已弃用。
陷阱3:Pages Router 和 App Router 可以共存——渐进式迁移的正确策略是新功能用 App Router,旧路由逐步迁移。