正文
Next.js 16 的迁移重点,不只是把依赖升级到最新版。真正容易卡住的地方,主要集中在两个方向:middleware.ts 改成 proxy.ts,以及缓存 API 从隐式缓存走向更明确的 Cache Components 模型。官方升级指南也把这两项列进了 Next.js 16 的核心迁移点:从 deprecated middleware 约定迁移到 proxy,并移除或稳定部分缓存相关 API。
这篇博客会用一份实战清单,帮你按顺序完成 Next.js 16 迁移。
一、先确认基础版本:Node、React、TypeScript 都要跟上
升级前先别急着改业务代码,先检查基础环境。Next.js 16 要求 Node.js 最低版本为 20.9.0,不再支持 Node.js 18;TypeScript 最低要求是 5.1.0,浏览器支持范围也提升到 Chrome 111+、Edge 111+、Firefox 111+、Safari 16.4+。
推荐先跑:
node -v
npm -v
pnpm -v手动升级可以这样做:
pnpm add next@latest react@latest react-dom@latest
pnpm add -D typescript@latest @types/react@latest @types/react-dom@latest或者使用官方 codemod:
pnpm dlx @next/codemod@canary upgrade latest官方说明这个升级 codemod 可以处理多个迁移任务,包括更新 Turbopack 配置、把 next lint 迁移到 ESLint CLI、把 middleware 约定迁移到 proxy、移除稳定 API 的 unstable_ 前缀,以及移除旧的 experimental_ppr 配置。
二、middleware.ts 改 proxy.ts:不是简单改文件名
Next.js 16 中,middleware.ts 被重命名为 proxy.ts。官方解释是,“middleware” 这个词容易让开发者联想到 Express.js middleware,从而误用它;改名为 Proxy 是为了更清楚地表达它位于应用网络边界,用于请求进入路由渲染前的拦截、重写、重定向、修改 header 或直接返回响应。
迁移前:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
export const config = {
matcher: '/dashboard/:path*',
}迁移后:
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
export const config = {
matcher: '/dashboard/:path*',
}官方也提供了单独的迁移命令:
npx @next/codemod@latest middleware-to-proxy .这个 codemod 会把 middleware.ts 重命名为 proxy.ts,并把导出的 middleware 函数改名为 proxy。
三、proxy.ts 应该放在哪里?
proxy.ts 应放在项目根目录,或者如果你使用 src 目录,就放在 src 下,位置要与 app 或 pages 同级。官方文档也提醒,如果你自定义了 pageExtensions,例如 .page.ts,那么文件名也要对应改成 proxy.page.ts。
常见结构如下:
my-next-app/
├─ app/
├─ proxy.ts
├─ next.config.ts
└─ package.json或者:
my-next-app/
├─ src/
│ ├─ app/
│ └─ proxy.ts
├─ next.config.ts
└─ package.jsonproxy.ts 可以使用命名导出:
export function proxy(request: NextRequest) {
return NextResponse.next()
}也可以使用默认导出:
export default function proxy(request: NextRequest) {
return NextResponse.next()
}但一个 Proxy 文件只能导出一个 Proxy 函数,不能在同一个文件里放多个 Proxy。
四、迁移 Proxy 时最容易踩的坑
迁移 Proxy 时,不要只看文件名是否改完。真正容易出问题的是运行时、matcher 和安全边界。
不要在 proxy.ts 里设置 runtime
Next.js 16 的 Proxy 默认使用 Node.js runtime,而且 Proxy 文件里不支持 runtime 配置;如果你在 Proxy 中设置 runtime,会直接报错。
错误写法:
export const runtime = 'edge'正确做法是删除它:
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
return NextResponse.next()
}不要把 Proxy 当成万能鉴权层
Proxy 很适合做重定向、国际化入口、轻量鉴权判断、灰度分流和 header 处理。但官方文档也提醒,Server Functions 是按所在路由的 POST 请求处理的;如果你的 matcher 排除了某个路径,相关 Server Function 也可能跳过 Proxy 覆盖。因此,敏感的鉴权和授权逻辑不应该只依赖 Proxy,还应在对应的 Server Function 内部再次校验。
matcher 必须是静态常量
matcher 需要能在构建阶段被静态分析,动态变量会被忽略。推荐这样写:
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
}不要这样写:
const protectedRoutes = ['/dashboard/:path*']
export const config = {
matcher: protectedRoutes,
}五、缓存 API 最大变化:从隐式缓存变成显式缓存
Next.js 16 的缓存思路更清晰:以前很多缓存行为是隐式的,开发者常常需要猜页面到底是静态、动态、缓存、还是重新验证。Next.js 16 推出 Cache Components,把缓存变成更明确的 opt-in 模型,核心是 "use cache" 指令、cacheLife()、cacheTag()、revalidateTag() 和 updateTag()。官方发布说明也写到,Cache Components 让缓存更显式、更灵活,并通过 "use cache" 缓存页面、组件和函数。
启用方式:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig启用 Cache Components 后,旧的路由段配置,例如 dynamic、revalidate、fetchCache,会被 use cache 和 cacheLife 替代。
六、dynamic = "force-dynamic" 怎么改?
如果你启用了 Cache Components,很多过去用于“强制动态”的配置可以删除。
迁移前:
export const dynamic = 'force-dynamic'
export default async function Page() {
return <div>Dashboard</div>
}迁移后:
export default async function Page() {
return <div>Dashboard</div>
}原因是,在 Cache Components 模型下,页面默认按请求时动态执行;只有你明确使用 "use cache" 时,相关函数、组件或页面才会进入缓存模型。官方迁移文档也说明,dynamic = "force-dynamic" 不再需要,因为所有页面默认就是动态的。
七、revalidate 怎么改成 cacheLife()?
旧写法:
export const revalidate = 3600
export default async function Page() {
const posts = await getPosts()
return <PostList posts={posts} />
}新写法建议把缓存靠近数据获取逻辑:
import { cacheLife } from 'next/cache'
async function getPosts() {
'use cache'
cacheLife('hours')
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function Page() {
const posts = await getPosts()
return <PostList posts={posts} />
}cacheLife() 用于设置函数或组件的缓存生命周期,并且需要和 "use cache" 一起使用。官方文档强调,cacheLife() 应放在被缓存的函数或组件作用域内,即使 "use cache" 放在文件级别,也要确保每次函数调用只执行一个 cacheLife()。
八、unstable_cache 怎么改?
Next.js 16 中,unstable_cache 已经被 "use cache" 替代。官方文档明确建议启用 Cache Components,并用 "use cache" 替换 unstable_cache。
迁移前:
import { unstable_cache } from 'next/cache'
const getCachedUser = unstable_cache(
async (id: string) => getUser(id),
['user'],
{
tags: ['users'],
revalidate: 60,
}
)迁移后:
import { cacheLife, cacheTag } from 'next/cache'
async function getCachedUser(id: string) {
'use cache'
cacheTag('users', `user-${id}`)
cacheLife('minutes')
return getUser(id)
}这个新写法更直观:
'use cache'表示“这个函数结果要缓存”。cacheTag()表示“这份缓存属于哪些标签”。cacheLife()表示“缓存多久或按什么策略失效”。
九、cacheTag():给缓存打标签,方便按需失效
cacheTag() 的作用是给缓存数据添加标签,后续可以通过 revalidateTag() 或 updateTag() 精准刷新这些缓存。官方文档说明,cacheTag() 可以接收一个或多个字符串,单个自定义 tag 最长 256 个字符,最多 128 个 tag。
示例:
import { cacheTag, cacheLife } from 'next/cache'
export async function getPost(slug: string) {
'use cache'
cacheTag('posts', `post-${slug}`)
cacheLife('days')
return db.post.findUnique({
where: { slug },
})
}这样做的好处是,文章列表可以用 posts 标签,单篇文章可以用 post-${slug} 标签。更新单篇文章时,不一定要刷新所有内容。
十、revalidateTag() 怎么改?现在需要第二个参数
Next.js 16 中,revalidateTag() 的推荐写法发生了明显变化。它现在需要第二个参数,用来指定 cacheLife profile,比如 'max'、'hours'、'days',也可以传 { expire: number }。单参数写法 revalidateTag('posts') 已经被标记为 deprecated。
旧写法:
import { revalidateTag } from 'next/cache'
revalidateTag('posts')推荐新写法:
import { revalidateTag } from 'next/cache'
revalidateTag('posts', 'max')也可以这样写:
revalidateTag('products', { expire: 3600 })revalidateTag('posts', 'max') 使用 stale-while-revalidate 语义:用户先看到旧缓存,Next.js 在后台刷新新数据。这个模式适合博客、商品目录、文档、新闻列表等允许短暂延迟更新的内容。官方也推荐大多数场景使用 'max'。
十一、updateTag():用户提交表单后要立刻看到新数据
updateTag() 是 Next.js 16 新增的 Server Actions-only API,用于 “read-your-own-writes” 场景。简单说,就是用户刚刚提交了修改,下一次读取必须立刻看到新数据,而不是先看到旧缓存。
示例:
'use server'
import { updateTag } from 'next/cache'
export async function updateUserProfile(userId: string, formData: FormData) {
await db.user.update({
where: { id: userId },
data: {
name: String(formData.get('name')),
},
})
updateTag(`user-${userId}`)
}updateTag() 只能在 Server Actions 中调用,不能在 Route Handler、Client Component 或 Proxy 中使用。若你需要在 Route Handler 或 webhook 中刷新缓存,应使用 revalidateTag()。
十二、revalidateTag() 和 updateTag() 怎么选?
| 场景 | 推荐 API | 原因 |
|---|---|---|
| CMS webhook 通知内容更新 | revalidateTag(tag, 'max') | 适合后台刷新,用户可短暂看到旧内容 |
| 用户提交表单后立即看到结果 | updateTag(tag) | Server Action 内立即过期,下一次读取 fresh data |
| 商品目录更新 | revalidateTag('products', 'max') | 商品列表允许短暂延迟 |
| 用户资料、设置、订单状态 | updateTag() | 用户对一致性预期更高 |
| Route Handler 中处理外部回调 | revalidateTag() | updateTag() 不能在 Route Handler 中使用 |
一句话记忆: 用户自己刚改的数据,用 updateTag();内容平台或后台系统触发刷新,用 revalidateTag(tag, 'max')。
十三、refresh():只刷新未缓存数据,不碰缓存
Next.js 16 还新增了 refresh(),它也是 Server Actions-only API,但它不会操作缓存,只用于刷新未缓存的数据。官方示例中,它适合类似“标记通知已读后刷新 header 中未缓存的通知数量”的场景。
示例:
'use server'
import { refresh } from 'next/cache'
export async function markAsRead(id: string) {
await db.notification.update({
where: { id },
data: { read: true },
})
refresh()
}不要把 refresh() 当作 revalidateTag() 的替代品。它不负责清理或更新缓存。
十四、异步请求 API:旧同步写法要彻底清掉
Next.js 15 曾引入异步请求 API,并提供临时同步兼容。但 Next.js 16 开始,同步访问被完全移除。受影响的 API 包括 cookies()、headers()、draftMode(),以及 params、searchParams 等请求时数据。
旧写法:
import { cookies } from 'next/headers'
export default function Page() {
const token = cookies().get('token')
return <div>{token?.value}</div>
}新写法:
import { cookies } from 'next/headers'
export default async function Page() {
const cookieStore = await cookies()
const token = cookieStore.get('token')
return <div>{token?.value}</div>
}params 也要注意:
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <div>{slug}</div>
}这类问题非常常见,尤其是从 Next.js 14 或早期 Next.js 15 项目直接升级时。
十五、Turbopack 默认启用:构建失败不一定是 Next.js 代码问题
Next.js 16 中,Turbopack 已稳定,并默认用于 next dev 和 next build。以前你可能写了 next dev --turbopack 或 next build --turbopack,现在这些参数不再必要。
旧脚本:
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start"
}
}新脚本:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}如果项目里有自定义 webpack 配置,next build 现在默认使用 Turbopack,可能会为了避免配置误用而失败。你可以迁移到 Turbopack 配置,也可以临时用 next build --webpack 退回 webpack。
十六、完整迁移清单
| 检查项 | 要做什么 |
|---|---|
| Node.js | 升级到 Node.js 20.9+ |
| React | 升级 react 和 react-dom |
| TypeScript | 确保 TypeScript 5.1+ |
| Middleware | middleware.ts 改为 proxy.ts |
| 导出函数 | middleware() 改为 proxy() |
| Proxy runtime | 删除 proxy.ts 中的 runtime 配置 |
| Matcher | 确保 matcher 是静态常量 |
| Cache Components | 在 next.config.ts 中启用 cacheComponents: true |
revalidate | 改为 'use cache' + cacheLife() |
unstable_cache | 改为 'use cache' |
| 缓存标签 | 使用 cacheTag() 标记缓存 |
revalidateTag | 改为 revalidateTag(tag, 'max') 或自定义 profile |
| 用户提交后刷新 | Server Action 中使用 updateTag() |
| 未缓存数据刷新 | Server Action 中使用 refresh() |
| Async APIs | cookies()、headers()、params、searchParams 改为异步访问 |
| Turbopack | 检查自定义 webpack 配置和 Sass ~ 导入 |
FAQ:Next.js 16 迁移常见问题
1. middleware.ts 还能继续用吗?
可以暂时用,但它已经 deprecated。Next.js 16 官方说明,middleware.ts 仍可用于 Edge runtime 场景,但未来版本会移除,建议尽快迁移到 proxy.ts。
2. proxy.ts 和旧 middleware.ts 的逻辑一样吗?
大部分逻辑可以保持一致。主要改动是文件名从 middleware.ts 改为 proxy.ts,导出函数从 middleware 改为 proxy。不过要注意,Next.js 16 的 Proxy 默认使用 Node.js runtime,且不允许在 Proxy 文件中设置 runtime。
3. revalidateTag('posts') 还能用吗?
单参数形式已经 deprecated。推荐改成:
revalidateTag('posts', 'max')如果你需要立即过期行为,并且代码在 Server Action 中,优先考虑 updateTag()。
4. updateTag() 可以放在 Route Handler 里吗?
不可以。updateTag() 只能在 Server Actions 中使用。Route Handler、Proxy、Client Component 中都不能调用它。Route Handler 里应使用 revalidateTag()。
5. unstable_cache 必须立刻删除吗?
不一定要在第一天全部删除,但 Next.js 16 官方已经建议使用 Cache Components 和 "use cache" 替代 unstable_cache。新代码不建议继续使用 unstable_cache。
6. 升级 Next.js 16 后为什么构建突然失败?
常见原因包括:Node 版本太低、同步访问 cookies() 或 headers()、params 没有 await、自定义 webpack 配置与默认 Turbopack 冲突、middleware.ts 没有迁移、revalidateTag() 仍用单参数写法。
结论:Next.js 16 迁移的核心是“边界更清楚,缓存更明确”
《Next.js 16 迁移清单:middleware.ts 改 proxy.ts、缓存 API 怎么改》的重点可以总结成两句话:
第一,middleware.ts 改成 proxy.ts,是为了让请求拦截的网络边界更清楚。 你要改文件名、改导出函数、检查 matcher,并避免把 Proxy 当成唯一安全边界。
第二,缓存 API 的变化,是为了让缓存从“框架隐式猜测”变成“开发者显式声明”。 'use cache' 表示要缓存,cacheLife() 表示缓存生命周期,cacheTag() 表示缓存标签,revalidateTag() 负责后台再验证,updateTag() 负责用户写入后的立即一致性。
按这份清单迁移,Next.js 16 升级就不会只是“修报错”,而是一次把项目请求层、缓存层和构建层重新梳理清楚的机会。