如何升级至版本 15

从 14 升级至 15

要升级至 Next.js 15 版本,可使用 upgrade 代码修改工具:

终端
npx @next/codemod@canary upgrade latest

若选择手动升级,请确保安装最新版本的 Next 和 React:

终端
npm i next@latest react@latest react-dom@latest eslint-config-next@latest

须知:

  • 若出现依赖冲突警告,可能需要将 reactreact-dom 更新至建议版本,或使用 --force--legacy-peer-deps 标志忽略警告。待 Next.js 15 和 React 19 均稳定后此操作将不再必要。

React 19

  • reactreact-dom 的最低版本要求现为 19
  • useFormState 已被 useActionState 取代。useFormState 在 React 19 中仍可用,但已被弃用并将在未来版本移除。推荐使用 useActionState,它新增了直接读取 pending 状态等特性。了解更多
  • useFormStatus 新增了 datamethodaction 等字段。若未使用 React 19,则仅 pending 字段可用。了解更多
  • 详见 React 19 升级指南

须知: 若使用 TypeScript,请同时升级 @types/react@types/react-dom 至最新版本。

异步请求 API (重大变更)

原先依赖运行时信息的同步动态 API 现改为异步

为降低迁移成本,我们提供了代码修改工具来自动化此过程,且这些 API 可暂时以同步方式访问。

cookies

推荐异步用法

import { cookies } from 'next/headers'

// 升级前
const cookieStore = cookies()
const token = cookieStore.get('token')

// 升级后
const cookieStore = await cookies()
const token = cookieStore.get('token')

临时同步用法

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

// 升级前
const cookieStore = cookies()
const token = cookieStore.get('token')

// 升级后
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies
// 开发环境下会显示警告
const token = cookieStore.get('token')

headers

推荐异步用法

import { headers } from 'next/headers'

// 升级前
const headersList = headers()
const userAgent = headersList.get('user-agent')

// 升级后
const headersList = await headers()
const userAgent = headersList.get('user-agent')

临时同步用法

import { headers, type UnsafeUnwrappedHeaders } from 'next/headers'

// 升级前
const headersList = headers()
const userAgent = headersList.get('user-agent')

// 升级后
const headersList = headers() as unknown as UnsafeUnwrappedHeaders
// 开发环境下会显示警告
const userAgent = headersList.get('user-agent')

draftMode

推荐异步用法

import { draftMode } from 'next/headers'

// 升级前
const { isEnabled } = draftMode()

// 升级后
const { isEnabled } = await draftMode()

临时同步用法

import { draftMode, type UnsafeUnwrappedDraftMode } from 'next/headers'

// 升级前
const { isEnabled } = draftMode()

// 升级后
// 开发环境下会显示警告
const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode

paramssearchParams

异步布局

// 升级前
type Params = { slug: string }

export function generateMetadata({ params }: { params: Params }) {
  const { slug } = params
}

export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}

// 升级后
type Params = Promise<{ slug: string }>

export async function generateMetadata({ params }: { params: Params }) {
  const { slug } = await params
}

export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = await params
}

同步布局

// 升级前
type Params = { slug: string }

export default function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}

// 升级后
import { use } from 'react'

type Params = Promise<{ slug: string }>

export default function Layout(props: {
  children: React.ReactNode
  params: Params
}) {
  const params = use(props.params)
  const slug = params.slug
}

异步页面

// 升级前
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }

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

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

// 升级后
type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>

export async function generateMetadata(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = await props.params
  const searchParams = await props.searchParams
  const slug = params.slug
  const query = searchParams.query
}

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

同步页面

'use client'

// 升级前
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }

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

// 升级后
import { use } from 'react'

type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>

export default function Page(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = use(props.params)
  const searchParams = use(props.searchParams)
  const slug = params.slug
  const query = searchParams.query
}
// 升级前
export default function Page({ params, searchParams }) {
  const { slug } = params
  const { query } = searchParams
}

// 升级后
import { use } from "react"

export default function Page(props) {
  const params = use(props.params)
  const searchParams = use(props.searchParams)
  const slug = params.slug
  const query = searchParams.query
}

路由处理器

app/api/route.ts
// 升级前
type Params = { slug: string }

export async function GET(request: Request, segmentData: { params: Params }) {
  const params = segmentData.params
  const slug = params.slug
}

// 升级后
type Params = Promise<{ slug: string }>

export async function GET(request: Request, segmentData: { params: Params }) {
  const params = await segmentData.params
  const slug = params.slug
}
app/api/route.js
// 升级前
export async function GET(request, segmentData) {
  const params = segmentData.params
  const slug = params.slug
}

// 升级后
export async function GET(request, segmentData) {
  const params = await segmentData.params
  const slug = params.slug
}

runtime 配置 (重大变更)

runtime 路由段配置 原先支持 experimental-edgeedge 两个值。由于两者功能相同,为简化选项,现在使用 experimental-edge 将报错。请将配置更新为 edge代码修改工具 可自动完成此操作。

fetch 请求

fetch 请求 默认不再缓存。

要为特定 fetch 请求启用缓存,可传递 cache: 'force-cache' 选项。

app/layout.js
export default async function RootLayout() {
  const a = await fetch('https://...') // 不缓存
  const b = await fetch('https://...', { cache: 'force-cache' }) // 缓存

  // ...
}

要为布局或页面中所有 fetch 请求启用缓存,可使用 fetchCache = 'default-cache' 路由段配置选项。若单个 fetch 请求指定了 cache 选项,则以该选项为准。

app/layout.js
// 作为根布局,应用中所有未设置 cache 选项的 fetch 请求都将被缓存
export const fetchCache = 'default-cache'

export default async function RootLayout() {
  const a = await fetch('https://...') // 缓存
  const b = await fetch('https://...', { cache: 'no-store' }) // 不缓存

  // ...
}

路由处理器

路由处理器 中的 GET 函数默认不再缓存。要为 GET 方法启用缓存,可在路由处理器文件中使用 dynamic = 'force-static' 等路由配置选项。

app/api/route.js
export const dynamic = 'force-static'

export async function GET() {}

客户端路由缓存

通过 <Link>useRouter 在页面间导航时,页面 段不再从客户端路由缓存中复用。但在浏览器前进/后退导航及共享布局时仍会复用。

要为页面段启用缓存,可使用 staleTimes 配置选项:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,
      static: 180,
    },
  },
}

module.exports = nextConfig

布局加载状态 仍会在导航时被缓存和复用。

next/font

@next/font 包已被移除,改用内置的 next/font代码修改工具 可安全自动地重命名导入。

app/layout.js
// 升级前
import { Inter } from '@next/font/google'

// 升级后
import { Inter } from 'next/font/google'

bundlePagesRouterDependencies

experimental.bundlePagesExternals 现已稳定并重命名为 bundlePagesRouterDependencies

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 升级前
  experimental: {
    bundlePagesExternals: true,
  },

  // 升级后
  bundlePagesRouterDependencies: true,
}

module.exports = nextConfig

serverExternalPackages

experimental.serverComponentsExternalPackages 现已稳定并重命名为 serverExternalPackages

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 升级前
  experimental: {
    serverComponentsExternalPackages: ['package-name'],
  },

  // 升级后
  serverExternalPackages: ['package-name'],
}

module.exports = nextConfig

速度分析

Next.js 15 移除了速度分析的自动检测功能。

要继续使用速度分析,请遵循 Vercel 速度分析快速入门 指南。

NextRequest 地理位置功能

NextRequest 上的 geoip 属性已被移除,因为这些值现在由您的托管服务提供商提供。我们提供了一个 代码迁移工具 (codemod) 来自动化此迁移过程。

如果您使用 Vercel 平台,可以改用 @vercel/functions 中的 geolocationipAddress 函数:

middleware.ts
import { geolocation } from '@vercel/functions'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const { city } = geolocation(request)

  // ...
}
middleware.ts
import { ipAddress } from '@vercel/functions'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const ip = ipAddress(request)

  // ...
}