前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Next.js 15 新特性完全指南 异步API与缓存策略的全部变化

首页2025-05-11 14:40:12Front-End
Next.jsReact前端开发React 19升级指南缓存

把一个 App Router 项目从 Next.js 14 升到 15,最先跳出来的往往不是报错,是数据库的监控曲线。原本靠 fetch 自动缓存扛住的列表接口,升级后每次请求都真的打到了后端,QPS 直接翻几倍。紧接着才是编辑器里那一片红,cookies() 拿不到值了,params.slug 变成了 undefined。

这两件事都不是 bug,是 Next.js 15 有意为之的行为变更。这篇把 15 的核心改动逐条拆开,讲清楚改法、背后的原因,以及哪些可以先放着不动。读完你会有一份能直接排期的迁移清单,而不是只知道「要加 await」。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 升级前的环境要求和官方 codemod 能覆盖的范围
  • React 19 带来的 useActionState、useFormStatus 增强和 Actions
  • 异步 Request API 的完整清单,以及 layout 里怎么用 use 保持同步
  • fetch 默认不缓存这一刀砍下去,你的项目该怎么补
  • 客户端路由缓存调整和 staleTimes 配置
  • @next/font、experimental-edge、NextRequest.geo 等一批 API 变更
  • 一套渐进式迁移的节奏建议

# 一、升级前的准备工作

Next.js 15 的环境要求比 14 高一档,但没到 16 那种「Node 18 直接不给跑」的程度:

  • Node.js 18.17.0 或更高
  • 用 TypeScript 的话,@types/react 和 @types/react-dom 要跟着升
  • 包管理器建议 pnpm 8+、npm 10+ 或 yarn 1.22+

类型包这条别跳过。React 19 的类型定义变化不小,老版本 @types/react 配新版 React 会在 JSX 返回值、ref 类型这些地方报一堆看起来毫无道理的错,找半天原因才发现是类型包没动。

# 一键升级

官方 codemod 能吃掉大部分机械劳动:

# 使用 pnpm
pnpm dlx @next/codemod@canary upgrade latest

# 使用 npm
npx @next/codemod@canary upgrade latest

# 使用 yarn
yarn dlx @next/codemod@canary upgrade latest

# 使用 bun
bunx @next/codemod@canary upgrade latest
@前端进阶之旅: 代码已经复制到剪贴板

它主要处理三类事,异步 API 的 await 补全、@next/font 到 next/font 的导入替换、以及几个配置项的重命名。剩下的业务逻辑调整还是得人工过。

# 手动升级

# pnpm
pnpm add next@latest react@latest react-dom@latest eslint-config-next@latest

# npm
npm install next@latest react@latest react-dom@latest eslint-config-next@latest

# yarn
yarn add next@latest react@latest react-dom@latest eslint-config-next@latest
@前端进阶之旅: 代码已经复制到剪贴板

注意:升级过程中如果遇到 peer dependencies 报警,可能需要手动指定 React 版本,或者临时用 --force / --legacy-peer-deps 绕过。生态里的第三方库跟上 React 19 需要时间,这类警告在过渡期很常见。

# 二、React 19 全面支持

Next.js 15 把 React 和 ReactDOM 的最低版本抬到了 19,所以 19 的新能力可以直接用。对日常业务影响最大的是表单这一块。

# useFormState 换成 useActionState

useFormState 被 useActionState 取代,前者还能用但已经标记废弃。新 API 最实用的改动是把 pending 状态直接返回出来了:

import { useActionState } from 'react'

async function submitForm(prevState, formData) {
  const result = await fetch('/api/submit', {
    body: formData
  })
  return result.json()
}

export default function Form() {
  const [state, submitAction, isPending] = useActionState(submitForm, null)

  return (
    <form action={submitAction}>
      {/* 表单字段 */}
      <button type="submit" disabled={isPending}>
        {isPending ? '提交中...' : '提交'}
      </button>
    </form>
  )
}
@前端进阶之旅: 代码已经复制到剪贴板

以前拿 pending 得往下再套一层组件用 useFormStatus,因为那个 Hook 必须在 form 的子树里才生效。现在同一层就能拿到,为了一个 loading 态而拆组件的日子结束了。

这个改动看着小,实际上它改变了表单组件的组织方式。

# useFormStatus 拿到了更多信息

import { useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending, data, method, action } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      {pending ? '提交中...' : '提交'}
    </button>
  )
}
@前端进阶之旅: 代码已经复制到剪贴板

多出来的 data、method、action 用在做通用提交按钮组件时很方便,比如根据 method 决定按钮文案,或者从 data 里读出正在提交的条目 ID 做局部高亮。

提示:如果还没升到 React 19,useFormStatus 依然只有 pending 一个属性。

React 19 本身还有 use、ref 作为 prop、文档元数据托管这些改动,我在 React 19 新特性 那篇里单独写过。

# 三、异步 Request API

这是 15 里最影响面积的改动。原本同步的动态 API 全部改成异步。

# cookies

import { cookies } from 'next/headers'

// Next.js 14,同步
const cookieStore = cookies()
const token = cookieStore.get('token')

// Next.js 15,异步
const cookieStore = await cookies()
const token = cookieStore.get('token')
@前端进阶之旅: 代码已经复制到剪贴板

如果一时改不完,官方给了一个过渡用的类型转换:

import { cookies, type UnsafeUnwrappedCookies } from 'next/headers'

// 临时同步用法,开发模式会打警告
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies
const token = cookieStore.get('token')
@前端进阶之旅: 代码已经复制到剪贴板

名字里带 Unsafe 就是在提醒你这是个临时口子。Next.js 16 已经把它彻底移除了,所以别把它当长期方案,升级到 15 的同时就该把这些点排进计划。

# headers 和 draftMode

import { headers } from 'next/headers'

// Next.js 14
const headersList = headers()
const userAgent = headersList.get('user-agent')

// Next.js 15
const headersList = await headers()
const userAgent = headersList.get('user-agent')
@前端进阶之旅: 代码已经复制到剪贴板
import { draftMode } from 'next/headers'

// Next.js 14
const { isEnabled } = draftMode()

// Next.js 15
const { isEnabled } = await draftMode()
@前端进阶之旅: 代码已经复制到剪贴板

那为什么非要改成异步呢?这几个 API 读的都是当次请求才存在的信息。同步读取意味着渲染必须停在这里等,整棵组件树被一个 UA 判断卡住。改成 Promise 之后,React 可以在等待请求信息落定的同时先去渲染不依赖它的分支,并发渲染和后来的 PPR 都建立在这个前提上。

# params 和 searchParams

layout 和 page 组件里的 params、searchParams 也变成了 Promise:

// Next.js 14
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }

export default async function Page({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}

// Next.js 15
type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>

export default async function Page(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = await props.params
  const searchParams = await props.searchParams
  const { slug } = params
  const { query } = searchParams
}
@前端进阶之旅: 代码已经复制到剪贴板

这里有个容易漏的地方。TypeScript 不会拦住你写 props.params.slug,因为 Promise 上确实没有 slug,但如果你的类型标注还停在旧版本,编译期一片安静,运行时才拿到 undefined。所以类型包必须同步升级,这一条前面提过一次,这里再强调一遍。

# layout 里想保持同步就用 use

不是所有组件都适合改成 async,尤其是 layout。React 19 的 use Hook 可以在同步组件里解开 Promise:

fe
  • 一、升级前的准备工作
    • 一键升级
    • 手动升级
  • 二、React 19 全面支持
    • useFormState 换成 useActionState
    • useFormStatus 拿到了更多信息
  • 三、异步 Request API
    • cookies
    • headers 和 draftMode
    • params 和 searchParams
    • layout 里想保持同步就用 use
    • Route Handler 里的 params
  • 四、fetch 默认不再缓存
    • 整段批量开缓存
    • Route Handler 的 GET 同样受影响
  • 五、客户端路由缓存策略调整
  • 六、其他 API 变更
    • @next/font 包被移除
    • experimental-edge 废弃
    • 两个实验配置转正
    • NextRequest 上的 geo 和 ip 没了
    • Speed Insights 自动埋点移除
  • 七、迁移节奏建议
  • 总结
  • 参考

← Next.js App Router 避坑指南 10个高频错误与正确写法React 19 新特性全解析与升级避坑指南 →