第 26 章

Next.js 15 新特性全解与升级指南

第26章:Next.js 15 新特性全解与升级指南

Next.js 15 不是简单的版本迭代——async cookies/headers、fetch 缓存默认行为反转、React 19 集成——这是过去几年最重要的一次版本发布。

本章核心问题:哪些破坏性变更必须立刻处理?React 19 带来了哪些新特性?Pages Router 如何渐进式迁移到 App Router?

读完本章你将理解


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 阶段已经正式稳定。官方基准测试显示:

重要限制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> 的区别:

适合搜索框、筛选表单等"导航型"表单(提交后改变 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 破坏性变更检查清单

升级前逐项确认:

必须处理

// 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
  // ...
}

建议处理

可选采用

执行升级:

npx @next/upgrade

这个命令会自动将 nextreactreact-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,旧路由逐步迁移。

本章评分
4.6  / 5  (4 评分)

💬 留言讨论