把一个跑在 Next.js 15 上的 App Router 项目直接装上 next@16,next dev 大概率还能起来,next build 却会直接失败。问题不在业务代码,在 next.config 里那几行自定义 webpack 配置。Next.js 16 把 Turbopack 提成了默认构建器,顺手把 15 时代留下的一堆兼容口子全关了,异步 API 的同步降级、middleware 的文件名、experimental_ppr 的路由级开关,这次一并收走。
这篇按官方升级文档把 16 的改动过了一遍,排序标准只有一个,就是「会不会让你的构建直接红掉」。每条都写清楚改法、为什么这么改、以及什么情况下可以先不动。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 升级前必须先满足的环境基线,以及 codemod 能帮你干掉多少活
- Turbopack 默认启用之后,自定义 webpack 配置的三条出路
- 异步 Request API 彻底移除同步兼容,
params/searchParams/id全都变 Promise - React 19.2 与 React Compiler 稳定版怎么开,代价是什么
- 缓存 API 新增的
updateTag和refresh,和老的revalidateTag怎么分工 middleware改名proxy带来的运行时限制next/image一口气改掉的七个默认值- 其余会让构建失败的零碎改动,以及被移除的功能
# 一、升级前先把环境和退路准备好
Next.js 16 抬高了运行环境的门槛,这一步没过,后面全都白搭。
| 要求 | 变化详情 |
|---|---|
| Node.js | 最低 20.9.0,且必须是 LTS,Node.js 18 不再支持 |
| TypeScript | 最低 5.1.0 |
| 浏览器 | Chrome 111+、Edge 111+、Firefox 111+、Safari 16.4+ |
Node 18 被砍掉这条最容易在 CI 上翻车。本地 nvm 里装的是 20,Dockerfile 里的基础镜像还写着 node:18-alpine,本地构建绿了推上去照样挂。升级第一件事是把 CI 镜像、Dockerfile、package.json 里的 engines 字段一起改掉,别只改本地。
浏览器基线抬到 Chrome 111 意味着产物里会直接使用更新的语法特性,如果你的用户群里还有大量老设备,这一条要单独评估,它不像 Node 版本那样有明显报错,只会在特定机型上白屏。
# 官方 codemod 能帮你做的事
Next.js 提供的升级 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
它会自动处理这几件事:
- 把
next.config.js里的 turbopack 配置改成新写法 - 从
next lint迁移到 ESLint CLI - 把废弃的
middleware约定迁移到proxy - 去掉已稳定 API 上的
unstable_前缀 - 移除 layout 和 page 里的
experimental_ppr配置
跑完 codemod 一定要看 diff。我的经验是它对标准写法处理得很干净,但对那种被包了一层工具函数的动态 API 调用会漏掉,最后还是得自己搜一遍。
# AI 辅助升级
如果你在用支持 MCP 的 AI 编码工具,官方还提供了 Next.js DevTools MCP:
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": ["-y", "next-devtools-mcp@latest"]
}
}
}
配好之后直接让助手帮你升级即可。这块我只在小项目上试过,几千个文件的老仓库有没有坑我没验证。
# 手动升级
不想跑 codemod 就手动装:
# pnpm
pnpm add next@latest react@latest react-dom@latest
# npm
npm install next@latest react@latest react-dom@latest
# yarn
yarn add next@latest react@latest react-dom@latest
注意:用 TypeScript 的项目记得同步升
@types/react和@types/react-dom,否则异步params的类型会一路飘红。
# 二、Turbopack 默认启用后的三条出路
Turbopack 在 16 里正式稳定,next dev 和 next build 默认都走它。以前那个 --turbopack / --turbo 标志可以从脚本里删掉了:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
# 自定义 webpack 配置会让构建失败
这是升级路上第一个真正的拦路虎。项目里只要还有 webpack(config) { ... } 这样的自定义配置,next build 就会直接失败,而不是默默忽略。官方这么设计是有道理的,静默忽略一份配置比报错危险得多,你的 alias、loader、externals 悄无声息地失效,问题会在运行时以最难排查的形态冒出来。
三条出路:
- 继续用 Turbopack 但忽略旧配置,跑
next build --turbopack - 把 webpack 配置翻译成 Turbopack 的对应选项,一劳永逸
- 退回 webpack,构建脚本加
--webpack标志
{
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"start": "next start"
}
}
我自己维护的一个站升到 16 之后走的就是第三条,构建脚本里保留了 --webpack。不是说 Turbopack 不行,而是那份 webpack 配置里挂了几个自定义 loader,翻译成本高于收益,先把版本升上去,构建器的迁移单独排期。这种「分两步走」的思路在大项目上比一次性梭哈稳得多。
# 配置从 experimental 提到了顶层
experimental.turbopack 移出实验区,现在是顶层字段:
import type { NextConfig } from 'next'
// Next.js 15 写法
const nextConfig: NextConfig = {
experimental: {
turbopack: {},
},
}
// Next.js 16 写法
const nextConfig: NextConfig = {
turbopack: {},
}
# 文件系统缓存还在 Beta
Turbopack 现在支持开发模式的文件系统缓存,把编译产物存到磁盘,重启 dev server 之后不用从头编一遍:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
turbopackFileSystemCacheForDev: true,
},
}
它还挂在 experimental 下面,说明官方自己也没打包票。冷启动慢是 Turbopack 目前最被吐槽的点,这个开关就是冲着它去的,值得开来试试,但别在 CI 上依赖它。
# Sass 的波浪号前缀不能用了
Turbopack 完整支持从 node_modules 导入 Sass,但 webpack 时代那个 ~ 前缀语法不认了:
// Webpack 写法,Turbopack 下报错
@import '~bootstrap/dist/css/bootstrap.min.css';
// 现在的写法
@import 'bootstrap/dist/css/bootstrap.min.css';
这个坑很隐蔽,因为报错信息指向的是找不到文件,第一直觉会去怀疑依赖没装。老项目里的样式文件往往是几年前写的,~ 散落在十几个 .scss 里,全局搜一遍 @import '~ 是最快的办法。
# 三、异步 Request API 这次真的没有退路了
Next.js 15 把 cookies、headers、draftMode 改成异步的时候,留了一个同步访问的过渡期,开发模式下打印警告但不报错。16 把这个口子彻底关掉了。
需要异步访问的完整清单:
cookiesheadersdraftModelayout.js、page.js、route.js里的paramspage.js里的searchParams
// Next.js 15,过渡期还能这么写
const cookieStore = cookies()
// Next.js 16,必须 await
const cookieStore = await cookies()
为什么非要异步?因为这些值只有在真实请求到来时才能确定,同步读取会强制整棵树在这一点上等住。改成 Promise 之后,React 的并发渲染才能在等待请求信息的同时继续渲染别的分支。关于这套异步化改造的来龙去脉,我在 Next.js 15 新特性完全指南 里写得更细。
回到升级本身,这块强烈建议交给 codemod,手改几百处 await 太容易漏。
# 用 typegen 生成类型助手
异步 params 最烦的不是加 await,是类型。手写 Promise<{ slug: string }> 既啰嗦又容易和路由实际结构对不上。16 里跑一次 npx next typegen,就能拿到全局可用的类型助手:
PageProps,页面组件属性LayoutProps,布局组件属性RouteContext,路由上下文
export default async function Page(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params
const query = await props.searchParams
return <h1>Blog Post: {slug}</h1>
}
这个设计是真的舒服。路由路径字符串直接当泛型参数,slug 的类型是从文件结构推出来的,改了目录名类型立刻报错,比手写接口靠谱得多。
# 图片元数据和 sitemap 里的参数也变了
容易被漏掉的是这两处。传给 opengraph-image、twitter-image、icon、apple-icon 的 props 现在都是 Promise: