正文

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+。

推荐先跑:

手动升级可以这样做:

或者使用官方 codemod:

官方说明这个升级 codemod 可以处理多个迁移任务,包括更新 Turbopack 配置、把 next lint 迁移到 ESLint CLI、把 middleware 约定迁移到 proxy、移除稳定 API 的 unstable_ 前缀,以及移除旧的 experimental_ppr 配置。

Bash
node -v
npm -v
pnpm -v
Bash
pnpm add next@latest react@latest react-dom@latest
pnpm add -D typescript@latest @types/react@latest @types/react-dom@latest
Bash
pnpm dlx @next/codemod@canary upgrade latest

二、middleware.ts 改 proxy.ts:不是简单改文件名

Next.js 16 中,middleware.ts 被重命名为 proxy.ts。官方解释是,“middleware” 这个词容易让开发者联想到 Express.js middleware,从而误用它;改名为 Proxy 是为了更清楚地表达它位于应用网络边界,用于请求进入路由渲染前的拦截、重写、重定向、修改 header 或直接返回响应。

迁移前:

迁移后:

官方也提供了单独的迁移命令:

这个 codemod 会把 middleware.ts 重命名为 proxy.ts,并把导出的 middleware 函数改名为 proxy

TypeScript
// 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*',
}
TypeScript
// 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*',
}
Bash
npx @next/codemod@latest middleware-to-proxy .

三、proxy.ts 应该放在哪里?

proxy.ts 应放在项目根目录,或者如果你使用 src 目录,就放在 src 下,位置要与 apppages 同级。官方文档也提醒,如果你自定义了 pageExtensions,例如 .page.ts,那么文件名也要对应改成 proxy.page.ts

常见结构如下:

或者:

proxy.ts 可以使用命名导出:

也可以使用默认导出:

但一个 Proxy 文件只能导出一个 Proxy 函数,不能在同一个文件里放多个 Proxy。

txt
my-next-app/
├─ app/
├─ proxy.ts
├─ next.config.ts
└─ package.json
txt
my-next-app/
├─ src/
│  ├─ app/
│  └─ proxy.ts
├─ next.config.ts
└─ package.json
TypeScript
export function proxy(request: NextRequest) {
  return NextResponse.next()
}
TypeScript
export default function proxy(request: NextRequest) {
  return NextResponse.next()
}

四、迁移 Proxy 时最容易踩的坑

迁移 Proxy 时,不要只看文件名是否改完。真正容易出问题的是运行时、matcher 和安全边界。

不要在 proxy.ts 里设置 runtime

Next.js 16 的 Proxy 默认使用 Node.js runtime,而且 Proxy 文件里不支持 runtime 配置;如果你在 Proxy 中设置 runtime,会直接报错。

错误写法:

正确做法是删除它:

TypeScript
export const runtime = 'edge'
TypeScript
// 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 需要能在构建阶段被静态分析,动态变量会被忽略。推荐这样写:

不要这样写:

TypeScript
export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*'],
}
TypeScript
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" 缓存页面、组件和函数。

启用方式:

启用 Cache Components 后,旧的路由段配置,例如 dynamicrevalidatefetchCache,会被 use cachecacheLife 替代。

TypeScript
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

六、dynamic = "force-dynamic" 怎么改?

如果你启用了 Cache Components,很多过去用于“强制动态”的配置可以删除。

迁移前:

迁移后:

原因是,在 Cache Components 模型下,页面默认按请求时动态执行;只有你明确使用 "use cache" 时,相关函数、组件或页面才会进入缓存模型。官方迁移文档也说明,dynamic = "force-dynamic" 不再需要,因为所有页面默认就是动态的。

TypeScript
export const dynamic = 'force-dynamic'

export default async function Page() {
  return <div>Dashboard</div>
}
TypeScript
export default async function Page() {
  return <div>Dashboard</div>
}

七、revalidate 怎么改成 cacheLife()?

旧写法:

新写法建议把缓存靠近数据获取逻辑:

cacheLife() 用于设置函数或组件的缓存生命周期,并且需要和 "use cache" 一起使用。官方文档强调,cacheLife() 应放在被缓存的函数或组件作用域内,即使 "use cache" 放在文件级别,也要确保每次函数调用只执行一个 cacheLife()

TypeScript
export const revalidate = 3600

export default async function Page() {
  const posts = await getPosts()
  return <PostList posts={posts} />
}
TypeScript
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} />
}

八、unstable_cache 怎么改?

Next.js 16 中,unstable_cache 已经被 "use cache" 替代。官方文档明确建议启用 Cache Components,并用 "use cache" 替换 unstable_cache

迁移前:

迁移后:

这个新写法更直观:

  • 'use cache' 表示“这个函数结果要缓存”。
  • cacheTag() 表示“这份缓存属于哪些标签”。
  • cacheLife() 表示“缓存多久或按什么策略失效”。
TypeScript
import { unstable_cache } from 'next/cache'

const getCachedUser = unstable_cache(
  async (id: string) => getUser(id),
  ['user'],
  {
    tags: ['users'],
    revalidate: 60,
  }
)
TypeScript
import { cacheLife, cacheTag } from 'next/cache'

async function getCachedUser(id: string) {
  'use cache'
  cacheTag('users', `user-${id}`)
  cacheLife('minutes')

  return getUser(id)
}

九、cacheTag():给缓存打标签,方便按需失效

cacheTag() 的作用是给缓存数据添加标签,后续可以通过 revalidateTag()updateTag() 精准刷新这些缓存。官方文档说明,cacheTag() 可以接收一个或多个字符串,单个自定义 tag 最长 256 个字符,最多 128 个 tag。

示例:

这样做的好处是,文章列表可以用 posts 标签,单篇文章可以用 post-${slug} 标签。更新单篇文章时,不一定要刷新所有内容。

TypeScript
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 },
  })
}

十、revalidateTag() 怎么改?现在需要第二个参数

Next.js 16 中,revalidateTag() 的推荐写法发生了明显变化。它现在需要第二个参数,用来指定 cacheLife profile,比如 'max''hours''days',也可以传 { expire: number }。单参数写法 revalidateTag('posts') 已经被标记为 deprecated。

旧写法:

推荐新写法:

也可以这样写:

revalidateTag('posts', 'max') 使用 stale-while-revalidate 语义:用户先看到旧缓存,Next.js 在后台刷新新数据。这个模式适合博客、商品目录、文档、新闻列表等允许短暂延迟更新的内容。官方也推荐大多数场景使用 'max'

TypeScript
import { revalidateTag } from 'next/cache'

revalidateTag('posts')
TypeScript
import { revalidateTag } from 'next/cache'

revalidateTag('posts', 'max')
TypeScript
revalidateTag('products', { expire: 3600 })

十一、updateTag():用户提交表单后要立刻看到新数据

updateTag() 是 Next.js 16 新增的 Server Actions-only API,用于 “read-your-own-writes” 场景。简单说,就是用户刚刚提交了修改,下一次读取必须立刻看到新数据,而不是先看到旧缓存。

示例:

updateTag() 只能在 Server Actions 中调用,不能在 Route Handler、Client Component 或 Proxy 中使用。若你需要在 Route Handler 或 webhook 中刷新缓存,应使用 revalidateTag()

TypeScript
'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}`)
}

十二、revalidateTag() 和 updateTag() 怎么选?

一句话记忆: 用户自己刚改的数据,用 updateTag();内容平台或后台系统触发刷新,用 revalidateTag(tag, 'max')

CMS webhook 通知内容更新`revalidateTag(tag, 'max')`
用户提交表单后立即看到结果`updateTag(tag)`
商品目录更新`revalidateTag('products', 'max')`
用户资料、设置、订单状态`updateTag()`
Route Handler 中处理外部回调`revalidateTag()`

十三、refresh():只刷新未缓存数据,不碰缓存

Next.js 16 还新增了 refresh(),它也是 Server Actions-only API,但它不会操作缓存,只用于刷新未缓存的数据。官方示例中,它适合类似“标记通知已读后刷新 header 中未缓存的通知数量”的场景。

示例:

不要把 refresh() 当作 revalidateTag() 的替代品。它不负责清理或更新缓存。

TypeScript
'use server'

import { refresh } from 'next/cache'

export async function markAsRead(id: string) {
  await db.notification.update({
    where: { id },
    data: { read: true },
  })

  refresh()
}

十四、异步请求 API:旧同步写法要彻底清掉

Next.js 15 曾引入异步请求 API,并提供临时同步兼容。但 Next.js 16 开始,同步访问被完全移除。受影响的 API 包括 cookies()headers()draftMode(),以及 paramssearchParams 等请求时数据。

旧写法:

新写法:

params 也要注意:

这类问题非常常见,尤其是从 Next.js 14 或早期 Next.js 15 项目直接升级时。

TypeScript
import { cookies } from 'next/headers'

export default function Page() {
  const token = cookies().get('token')
  return <div>{token?.value}</div>
}
TypeScript
import { cookies } from 'next/headers'

export default async function Page() {
  const cookieStore = await cookies()
  const token = cookieStore.get('token')

  return <div>{token?.value}</div>
}
TypeScript
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params

  return <div>{slug}</div>
}

十五、Turbopack 默认启用:构建失败不一定是 Next.js 代码问题

Next.js 16 中,Turbopack 已稳定,并默认用于 next devnext build。以前你可能写了 next dev --turbopacknext build --turbopack,现在这些参数不再必要。

旧脚本:

新脚本:

如果项目里有自定义 webpack 配置,next build 现在默认使用 Turbopack,可能会为了避免配置误用而失败。你可以迁移到 Turbopack 配置,也可以临时用 next build --webpack 退回 webpack。

JSON
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build --turbopack",
    "start": "next start"
  }
}
JSON
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

十六、完整迁移清单

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。推荐改成: 如果你需要立即过期行为,并且代码在 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() 仍用单参数写法。

TypeScript
revalidateTag('posts', 'max')

结论: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 升级就不会只是“修报错”,而是一次把项目请求层、缓存层和构建层重新梳理清楚的机会。

参考来源

Upgrading: Version 16Next.js DocsRenaming Middleware to ProxyNext.js DocsUpgrading: CodemodsNext.js DocsFile-system conventions: proxy.jsNext.js DocsNext.js 16Next.js BlogMigrating to Cache ComponentsNext.js DocsFunctions: cacheLifeNext.js DocsFunctions: unstable_cacheNext.js DocsFunctions: cacheTagNext.js DocsFunctions: revalidateTagNext.js DocsFunctions: updateTagNext.js Docs

相关文章

Nuxt 4 升级前先查什么:目录结构、useFetch、TypeScript 和模块兼容性工程实践 / 约 16 分钟Vite 7 升级卡住:为什么要求 Node 20.19+ / 22.12+工程实践 / 约 15 分钟Claude Code 项目上下文怎么检查开发环境 / 约 11 分钟7 个关键洞察:AI Coding 工具真正改变的不是写代码,而是验证代码智能编程 / 约 18 分钟