项目介绍

Next.js 是由 Vercel 公司开发并维护的开源 React 全栈框架。它在 React 的基础上提供了服务端渲染(SSR)、静态站点生成(SSG)、增量静态再生(ISR)、文件系统路由、API 路由等开箱即用的能力,是目前构建生产级 React 应用的首选框架。

Next.js 自 2016 年发布以来迅速成为 React 生态中最受欢迎的框架,被 TikTok、Twitch、Hulu、Nike、Notion 等知名公司采用。2023 年推出的 App Router 和 React Server Components 支持更是引领了全栈 React 开发的新范式。

项目信息详情
开发团队Vercel(美国旧金山)
GitHub Stars130,000+
官方网站nextjs.org
开源协议MIT
技术栈React / Node.js / TypeScript
当前版本Next.js 15(App Router 为默认)
包管理npm / yarn / pnpm / bun
一句话理解:Next.js 就是"React 的全栈升级版" — 它让 React 从一个前端 UI 库变成了可以处理路由、API、数据库、认证、部署的完整全栈框架。

Next.js vs Nuxt.js vs Remix vs Gatsby

对比维度 Next.js Nuxt.js Remix Gatsby
底层框架 React Vue.js React React
GitHub Stars 130k+ 55k+ 30k+ 55k+
渲染模式 SSR/SSG/ISR/CSR/Streaming SSR/SSG/ISR/CSR SSR/CSR/Streaming SSG 为主
Server Components 原生支持 不支持 实验性 不支持
路由系统 文件系统路由(App Router) 文件系统路由 文件系统路由 文件系统 + GraphQL
数据获取 fetch + 缓存 + revalidate useFetch / useAsyncData loader / action GraphQL 数据层
部署平台 Vercel / 任意 Node.js 任意 Node.js 任意 Node.js Gatsby Cloud / CDN
TypeScript 开箱即用 开箱即用 开箱即用 需配置
学习曲线 中等 较低(Vue 更简单) 中等 中等(GraphQL)
适用场景 全栈 Web 应用、SaaS Vue 生态全栈应用 数据密集型 Web 应用 博客、文档、营销站
选择建议:如果你用 React 且追求最完整的全栈能力和最大的社区生态,Next.js 是首选。Vue 用户选 Nuxt.js。注重 Web 标准和渐进增强选 Remix。纯静态内容站点可考虑 Gatsby。

快速开始

步骤 1 使用 create-next-app 创建项目

Next.js 官方提供的脚手架工具,一行命令即可创建完整项目:
# 使用 npx(推荐)
npx create-next-app@latest my-app

# 使用 yarn
yarn create next-app my-app

# 使用 pnpm
pnpm create next-app my-app

# 使用 bun
bun create next-app my-app
创建时会提示选择 TypeScript、ESLint、Tailwind CSS、App Router 等选项,建议全部选择"Yes"。

步骤 2 项目目录结构

使用 App Router 创建的 Next.js 项目结构如下:
my-app/
├── app/                  # App Router 路由目录
│   ├── layout.tsx        # 根布局(必需)
│   ├── page.tsx          # 首页 /
│   ├── globals.css       # 全局样式
│   ├── about/
│   │   └── page.tsx      # /about 页面
│   └── api/
│       └── hello/
│           └── route.ts  # API 路由 /api/hello
├── public/               # 静态资源
├── next.config.js        # Next.js 配置
├── package.json
├── tsconfig.json
└── tailwind.config.ts

步骤 3 启动开发服务器

进入项目目录,启动开发服务器:
cd my-app
npm run dev
访问 http://localhost:3000 即可看到 Next.js 的欢迎页面。开发服务器支持热模块替换(HMR),修改代码后浏览器自动刷新。

常用命令一览:
# 开发模式
npm run dev

# 生产构建
npm run build

# 启动生产服务器
npm run start

# 代码检查
npm run lint

核心特性

App Router

基于文件系统的路由,支持布局嵌套、加载状态、错误边界、并行路由、拦截路由等高级模式,是 Next.js 15 的默认路由方案。

React Server Components

组件默认在服务端运行,零客户端 JavaScript 开销。需要交互时使用 'use client' 指令切换为客户端组件。

SSR / SSG / ISR

同一项目中可混合使用服务端渲染、静态生成和增量静态再生,按页面粒度选择最优渲染策略。

API Routes

app/api/ 目录下创建 route.ts 文件即可定义 RESTful API,无需额外后端服务器。

Middleware

在请求到达页面之前执行逻辑,适用于认证、重定向、国际化、A/B 测试等场景。运行在 Edge Runtime 上,响应极快。

内置优化

自动图片优化(next/image)、字体优化(next/font)、脚本优化(next/script)、代码分割、预取链接等,开箱即用提升 Core Web Vitals。

请求生命周期: 浏览器请求 ──▶ Middleware(边缘)──▶ 路由匹配 │ ┌───────────────────┼───────────────────┐ ▼ ▼ ▼ Server Component Static Page API Route (服务端渲染) (CDN 缓存) (route.ts) │ │ │ ▼ ▼ ▼ Streaming HTML 返回 HTML 返回 JSON │ ▼ Client Component 水合(Hydration) │ ▼ 交互式页面就绪

示例 1 Server Component vs Client Component

App Router 中组件默认是 Server Component,需要在浏览器中使用 state、effect 或事件监听器时才添加 'use client'
// app/page.tsx — Server Component(默认)
// 可以直接 async、直接访问数据库、无需 useEffect
export default async function Home() {
  const data = await fetch('https://api.example.com/posts')
  const posts = await data.json()

  return (
    <main>
      <h1>博客文章</h1>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
      <LikeButton />  {/* 客户端组件 */}
    </main>
  )
}

// components/LikeButton.tsx — Client Component
'use client'
import { useState } from 'react'

export function LikeButton() {
  const [likes, setLikes] = useState(0)
  return <button onClick={() => setLikes(likes + 1)}>点赞 {likes}</button>
}
原则:尽可能让组件保持 Server Component,只在需要浏览器 API(useState、useEffect、onClick 等)时才使用 'use client'

路由系统

Next.js App Router 基于文件系统自动生成路由,app/ 目录中的文件夹结构即为 URL 路径结构。每个路由段对应一个文件夹,page.tsx 使其可公开访问。

路由 1 基础路由与文件约定

App Router 使用特殊的文件名约定来定义路由行为:
app/
├── page.tsx          # / 路由
├── layout.tsx        # 根布局(包裹所有页面)
├── loading.tsx       # 加载中 UI(Suspense 边界)
├── error.tsx         # 错误 UI(Error Boundary)
├── not-found.tsx     # 404 页面
├── about/
│   └── page.tsx      # /about
├── blog/
│   ├── page.tsx      # /blog
│   └── [slug]/
│       └── page.tsx  # /blog/:slug(动态路由)
└── (marketing)/      # 路由组(不影响 URL)
    ├── pricing/
    │   └── page.tsx  # /pricing
    └── contact/
        └── page.tsx  # /contact

路由 2 动态路由与参数

使用方括号定义动态路由段,支持多种匹配模式:
// app/blog/[slug]/page.tsx — 动态路由
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  return <article>{post.content}</article>
}

// 静态生成动态路由页面
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map((post) => ({ slug: post.slug }))
}
动态路由类型:
- [slug] — 匹配单个段:/blog/hello-world
- [...slug] — 匹配多个段:/docs/a/b/c
- [[...slug]] — 可选多段匹配:/docs/docs/a/b

路由 3 布局嵌套与模板

layout.tsx 在导航时保持状态不重新渲染,适合导航栏、侧边栏等持久 UI:
// app/layout.tsx — 根布局
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh-CN">
      <body>
        <nav>全站导航栏</nav>
        {children}
        <footer>全站页脚</footer>
      </body>
    </html>
  )
}

// app/dashboard/layout.tsx — 嵌套布局
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex">
      <aside>仪表盘侧边栏</aside>
      <main>{children}</main>
    </div>
  )
}
注意:如果需要在每次导航时重新挂载组件(重置状态),使用 template.tsx 替代 layout.tsx

路由 4 Loading 与 Error 处理

通过 loading.tsxerror.tsx 文件自动创建 Suspense 和 Error Boundary:
// app/dashboard/loading.tsx
export default function Loading() {
  return <div className="skeleton">加载中...</div>
}

// app/dashboard/error.tsx
'use client' // Error 组件必须是 Client Component

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>出错了!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>重试</button>
    </div>
  )
}

数据获取

Next.js App Router 中推荐直接在 Server Component 中使用 async/await 获取数据,并通过 fetch 选项控制缓存和重新验证策略。

数据 1 Server Component 中获取数据

Server Component 可以直接使用 async/await,无需 useEffect 或客户端状态管理:
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // ISR:每小时重新验证
  })
  if (!res.ok) throw new Error('获取文章失败')
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <a href={`/posts/${post.id}`}>{post.title}</a>
        </li>
      ))}
    </ul>
  )
}

数据 2 缓存策略与 Revalidation

Next.js 的 fetch 扩展了原生 API,提供三种缓存模式:
// 1. 静态数据(构建时获取,永久缓存)— 默认行为
const data = await fetch('https://api.example.com/data')

// 2. 定时重新验证(ISR)
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 } // 每 60 秒重新验证
})

// 3. 动态数据(每次请求都获取)
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store'
})

// 4. 基于标签的按需重新验证
import { revalidateTag } from 'next/cache'

const data = await fetch('https://api.example.com/data', {
  next: { tags: ['posts'] }
})

// 在 Server Action 中按需触发
revalidateTag('posts')
提示:在 Next.js 15 中,fetch 默认不缓存(cache: 'no-store')。如需缓存需显式指定 cache: 'force-cache' 或使用 next: { revalidate }

数据 3 Server Actions(服务端表单处理)

Server Actions 允许在客户端组件中直接调用服务端函数,无需手动创建 API 端点:
// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  await db.post.create({ data: { title, content } })
  revalidatePath('/posts')
}

// app/posts/new/page.tsx
import { createPost } from '@/app/actions'

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="标题" required />
      <textarea name="content" placeholder="内容" required />
      <button type="submit">发布</button>
    </form>
  )
}

部署方案

部署 1 Vercel 部署(推荐)

Vercel 是 Next.js 的官方托管平台,提供最佳的部署体验和零配置优化:
# 安装 Vercel CLI
npm i -g vercel

# 一键部署
vercel

# 或直接在 Vercel Dashboard 连接 GitHub 仓库
# 每次 push 自动触发部署
Vercel 自动支持 Edge Functions、ISR、Image Optimization、Analytics 等 Next.js 特性。免费版已足够个人项目使用。

部署 2 Docker 部署

使用 Docker 容器化部署,适合自托管场景:
# Dockerfile
FROM node:20-alpine AS base

FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]
需要在 next.config.js 中启用 standalone 输出:
// next.config.js module.exports = { output: 'standalone', }

部署 3 静态导出(Static Export)

如果不需要服务端功能,可以导出为纯静态 HTML,部署到任意 CDN/静态托管:
// next.config.js module.exports = { output: 'export', // 可选:自定义输出目录 distDir: 'dist', }
# 构建并导出
npm run build
# 静态文件在 out/ 目录(或自定义的 dist/)
限制:静态导出不支持 Server Components 的动态功能、API Routes、Middleware、ISR 等服务端特性。仅适合纯静态网站。

常见问题

以下是 Next.js 开发中最常见的 50 个问题与解决方案,覆盖构建错误、运行时问题、部署故障等。

水合与渲染错误 (#1 - #10)

#1 Hydration 不匹配 — 服务端与客户端 HTML 不一致

Error: Hydration failed because the server rendered HTML didn't match the client.
这是 Next.js 中最常见的错误。服务端渲染的 HTML 与客户端水合时生成的 HTML 不一致时触发。
常见原因与修复:
1. 使用了 Date.now()Math.random() 等非确定性值 → 改用 useEffect 在客户端设置
2. 浏览器扩展注入了额外 DOM 节点 → 使用 suppressHydrationWarning 属性
3. <p> 标签内嵌套了 <div> 等非法 HTML → 修复嵌套结构
4. 使用了 typeof window !== 'undefined' 条件渲染 → 改为 useEffect + state
// 错误做法
function Component() {
  return <p>{typeof window !== 'undefined' ? 'client' : 'server'}</p>
}

// 正确做法
'use client'
function Component() {
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])
  return <p>{mounted ? 'client' : 'server'}</p>
}

#2 客户端组件缺少 'use client' 指令

Error: useState only works in Client Components. Add the "use client" directive at the top of the file to use it.
App Router 中组件默认是 Server Component,不能使用 useStateuseEffectonClick 等客户端 API。
修复:在文件顶部添加 'use client' 指令。
'use client' // 必须是文件的第一行(注释除外)

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

#3 Server Component 不能传递函数给 Client Component

Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
Server Component 向 Client Component 传递的 props 必须可序列化(JSON 兼容),函数不可序列化。
解决方案:
1. 将回调函数改为 Server Action(添加 'use server'
2. 在 Client Component 内部定义函数
3. 使用事件处理器模式

#4 文本内容与服务端渲染不匹配

Warning: Text content did not match. Server: "2026-01-01" Client: "2026-01-02"
时区差异导致服务端和客户端日期显示不同。
修复:使用 suppressHydrationWarning 或在客户端格式化日期:
<time suppressHydrationWarning>
  {new Date().toLocaleDateString()}
</time>

#5 async Server Component 类型错误

Type error: 'Promise<Element>' is not a valid JSX element type.
TypeScript 版本或 @types/react 版本过低,不支持 async Server Component。
修复:升级依赖或在 tsconfig.json 中调整配置:
npm install @types/react@latest @types/react-dom@latest typescript@latest
或临时使用类型断言:{/* @ts-expect-error Async Server Component */}

#6 useSearchParams 导致整个页面取消静态渲染

Warning: useSearchParams() should be wrapped in a suspense boundary. This will cause the entire page to opt into client-side rendering.
useSearchParams() 在没有 Suspense 边界的情况下会使整个页面降级为客户端渲染。
修复:用 Suspense 包裹使用 useSearchParams 的组件:
import { Suspense } from 'react'

function SearchBar() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <SearchBarContent />
    </Suspense>
  )
}

#7 客户端组件中导入 Server-Only 模块

Error: You're importing a component that needs server-only. That only works in a Server Component.
在 Client Component 中导入了标记为 server-only 的模块(例如直接使用数据库客户端)。
修复:
1. 确保数据库查询等只在 Server Component 或 Server Action 中调用
2. 通过 props 将数据从 Server Component 传给 Client Component
3. 使用 server-only 包保护服务端代码:
// lib/db.ts
import 'server-only'
import { prisma } from './prisma'
export async function getUser(id: string) {
  return prisma.user.findUnique({ where: { id } })
}

#8 window is not defined

ReferenceError: window is not defined
在 Server Component 或服务端渲染期间访问了浏览器 API(windowdocumentlocalStorage)。
修复:
1. 将代码移到 Client Component 中并在 useEffect 内访问
2. 使用动态导入禁用 SSR:
import dynamic from 'next/dynamic'

const MapComponent = dynamic(() => import('./Map'), {
  ssr: false,
  loading: () => <p>地图加载中...</p>
})

#9 Streaming SSR 中断

Error: An error occurred in the Server Components render. The specific message is omitted in production builds.
Server Component 在流式渲染过程中抛出了未捕获的异常。
修复:
1. 添加 error.tsx 文件作为错误边界
2. 在数据获取中添加 try/catch
3. 开发模式下查看完整错误信息进行调试
4. 检查 Server Component 中的 async 操作是否正确 await

#10 第三方组件库 Hydration 错误

Error: Hydration failed because the initial UI does not match what was rendered on the server.
很多第三方 UI 库(如 Ant Design、MUI)未适配 Server Component,直接导入会引发水合错误。
修复:
1. 创建客户端包装组件:
'use client'
export { Button } from 'antd' // 重新导出为客户端组件

2. 或使用动态导入:dynamic(() => import('antd').then(mod => mod.Button), { ssr: false })

构建与编译错误 (#11 - #20)

#11 构建时 fetch 失败

Error: fetch failed - cause: connect ECONNREFUSED 127.0.0.1:3000
构建时(npm run build)调用了本地 API Route,但此时开发服务器未运行。
修复:
1. 直接调用数据库或业务逻辑函数,不要在 Server Component 中 fetch 自己的 API Route
2. 使用环境变量区分构建时和运行时的 API 地址
// 错误:Server Component 中 fetch 自己的 API
const res = await fetch('http://localhost:3000/api/posts')

// 正确:直接调用业务函数
import { getPosts } from '@/lib/posts'
const posts = await getPosts()

#12 Module not found — 路径别名无效

Module not found: Can't resolve '@/components/Header'
@/ 路径别名未正确配置。确保 tsconfig.json 中有正确的路径映射:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  }
}
create-next-app 默认会配置好,手动项目需自行添加。

#13 ESLint 规则导致构建失败

Failed to compile. ESLint: 'React' is defined but never used. (@typescript-eslint/no-unused-vars)
Next.js 构建时会运行 ESLint 检查,任何 error 级别的规则违反都会阻止构建。
修复:
1. 修复 ESLint 错误(推荐)
2. 临时忽略:在 next.config.js 中添加 eslint: { ignoreDuringBuilds: true }
3. 调整规则级别为 warning
警告:不建议长期忽略 ESLint 错误,它们通常指出了真实的代码问题。

#14 TypeScript 类型错误阻止构建

Type error: Type '{ title: string; }' is missing the following properties from type 'Post': id, content, createdAt
Next.js 构建时会进行类型检查,类型错误会阻止构建。
修复:
1. 修复类型错误(推荐)
2. 临时忽略:typescript: { ignoreBuildErrors: true }(不推荐)
3. 检查 @types/react@types/node 版本是否匹配

#15 构建输出体积过大

Warning: First Load JS shared by all is 350 kB (exceeds threshold of 250 kB)
首次加载的 JavaScript 包超过了推荐大小,影响页面加载性能。
优化方法:
1. 使用 @next/bundle-analyzer 分析包内容
2. 用 dynamic() 懒加载大组件
3. 检查是否在 Client Component 中导入了大型库的全部内容
4. 使用 tree-shaking 友好的导入:import { Button } from 'antd' 而非 import antd from 'antd'
npm install @next/bundle-analyzer
# 在 next.config.js 中配置后运行:
ANALYZE=true npm run build

#16 next.config.js 配置语法错误

Error: Invalid next.config.js options detected: unrecognized key "experimental.appDir"
Next.js 版本升级后某些配置项被移除或变更。experimental.appDir 在 Next.js 14+ 中已不需要(App Router 为默认)。
修复:删除过时的配置项,参考当前版本的官方文档。
// next.config.js(Next.js 15 常用配置)
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: '**.example.com' },
    ],
  },
}
module.exports = nextConfig

#17 Turbopack 与 Webpack 插件不兼容

Error: Turbopack does not support custom webpack configuration.
Next.js 15 默认在开发模式使用 Turbopack,但 Turbopack 不支持自定义 Webpack 配置。
修复:
1. 使用 next dev --turbo=false 回退到 Webpack
2. 将自定义 Webpack 配置迁移到 Turbopack 支持的方式
3. 检查第三方插件是否提供了 Turbopack 兼容版本

#18 CSS 模块导入报错

Error: CSS Modules cannot be imported from within node_modules.
Next.js 默认不允许从 node_modules 导入 CSS Modules。
修复:
1. 使用 transpilePackages 配置转译该包:
// next.config.js
module.exports = {
  transpilePackages: ['some-package'],
}

2. 或使用全局 CSS 导入(在 layout.tsxglobals.css 中)

#19 静态页面生成超时

Error: Static page generation for /blog/[slug] timed out after 60 seconds.
generateStaticParams 生成的页面数量过多,或数据获取过慢导致超时。
修复:
1. 增加超时时间:staticPageGenerationTimeout: 120
2. 减少 generateStaticParams 返回的页面数量,其余使用动态生成
3. 优化数据获取速度
4. 使用 dynamicParams = true 允许按需生成

#20 依赖版本冲突

Error: Cannot find module 'react-dom/client' - Require stack: ...
React 和 Next.js 的版本不匹配,或存在多个 React 实例。
修复:
# 确保 React 版本匹配
npm ls react react-dom

# 清理重装
rm -rf node_modules .next package-lock.json
npm install

# Next.js 15 需要 React 19
npm install react@latest react-dom@latest next@latest

路由与导航问题 (#21 - #30)

#21 404 页面 — 路由文件命名错误

页面访问返回 404 Not Found
App Router 中路由文件必须命名为 page.tsx(不是 index.tsx)。
常见错误:
- app/about/index.tsx → 应改为 app/about/page.tsx
- app/about/About.tsx → 应改为 app/about/page.tsx
- 文件夹名包含大写字母 → URL 区分大小写

#22 路由组与布局冲突

Error: You cannot have two parallel pages that resolve to the same path.
多个路由组 (group) 中定义了相同路径的 page.tsx
修复:确保每个 URL 路径只有一个对应的 page.tsx 文件。路由组用圆括号 () 包裹,不会体现在 URL 中,但同一路径不能重复定义。

#23 useRouter 导入错误

Error: NextRouter was not mounted.
App Router 和 Pages Router 使用不同的 useRouter
// App Router — 使用 next/navigation
import { useRouter } from 'next/navigation'

// Pages Router(旧版)— 使用 next/router
import { useRouter } from 'next/router'
如果在 App Router 项目中导入了 next/router,就会出现此错误。

#24 动态路由参数为 undefined

TypeError: Cannot read properties of undefined (reading 'slug')
Next.js 15 中 params 变为异步 Promise,需要 await:
// Next.js 15 正确写法
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  // ...
}

// Next.js 14 旧写法(不再适用于 15)
export default function Page({ params }: { params: { slug: string } }) {
  const { slug } = params
}

#25 middleware.ts 位置错误不生效

Middleware 文件存在但未被执行
middleware.ts 必须放在项目根目录(与 app/ 同级),不能放在 app/ 内部。
my-app/
├── middleware.ts    ✅ 正确位置
├── app/
│   ├── middleware.ts  ❌ 不会生效
│   └── page.tsx
同时确保 matcher 配置正确匹配了目标路由。

#26 并行路由(Parallel Routes)插槽为空

并行路由插槽渲染为空白,未显示预期内容
并行路由使用 @folder 语法定义插槽,必须在同级 layout.tsx 中接收:
// app/layout.tsx
export default function Layout({
  children,
  modal, // 对应 app/@modal/page.tsx
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <>
      {children}
      {modal}
    </>
  )
}
并且需要提供 default.tsx 作为未匹配时的回退内容。

#27 Link 组件客户端导航失效

点击 Link 后页面完整刷新而非客户端导航
确保使用 Next.js 的 Link 组件而非原生 <a> 标签:
// 正确 — 客户端导航
import Link from 'next/link'
<Link href="/about">关于我们</Link>

// 错误 — 完整页面刷新
<a href="/about">关于我们</a>
另外检查是否有 Middleware 进行了重定向(重定向会触发完整导航)。

#28 拦截路由(Intercepting Routes)不工作

拦截路由未拦截导航,直接跳转到了目标页面
拦截路由使用 (.)(..)(...) 语法,文件夹命名必须精确:
- (.)folder — 拦截同级路由
- (..)folder — 拦截上一级路由
- (..)(..)folder — 拦截上两级路由
- (...)folder — 从根路径拦截
确保拦截路由文件夹中有 page.tsx,且硬刷新(F5)会显示原始页面(拦截只在客户端导航时生效)。

#29 重定向循环

Error: NEXT_REDIRECT was called too many times (detected a redirect loop).
Middleware 或 redirect() 函数产生了无限重定向循环。
修复:
1. 检查 Middleware 中的 matcher 是否排除了重定向目标路径
2. 确保重定向条件不会形成循环
// middleware.ts — 正确的 matcher
export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico|login).*)',
  ],
}

#30 generateStaticParams 返回空数组

所有动态路由页面返回 404
generateStaticParams 返回空数组意味着没有预生成任何页面。确保函数正确返回参数数组,且 dynamicParams 设置为 true(默认)以允许按需生成:
// 确保返回正确的参数格式
export async function generateStaticParams() {
  const posts = await db.post.findMany()
  return posts.map((post) => ({
    slug: post.slug, // 键名必须与文件夹名 [slug] 匹配
  }))
}

// 允许按需生成未预渲染的页面
export const dynamicParams = true

数据获取与缓存问题 (#31 - #38)

#31 fetch 数据不更新(缓存问题)

数据始终返回旧值,即使数据源已更新
Next.js 14 默认缓存 fetch 结果。Next.js 15 默认不缓存,但如果显式设置了 force-cache 则会缓存。
修复:
// 禁用缓存(每次获取最新数据)
const res = await fetch(url, { cache: 'no-store' })

// 定时重新验证
const res = await fetch(url, { next: { revalidate: 60 } })

// 使用标签按需重新验证
const res = await fetch(url, { next: { tags: ['posts'] } })
// 在 Server Action 中:revalidateTag('posts')

#32 Server Action 返回值不可序列化

Error: Only plain objects, and a few built-ins, can be passed to Client Components from Server Actions.
Server Action 的返回值必须可序列化为 JSON。不能返回 Date 对象、Map、Set、类实例等。
修复:将返回值转换为纯对象或 JSON 兼容格式:
'use server'
export async function getUser(id: string) {
  const user = await db.user.findUnique({ where: { id } })
  return {
    ...user,
    createdAt: user.createdAt.toISOString(), // Date → string
  }
}

#33 cookies() / headers() 使整个路由变为动态

Route /dashboard opted into dynamic rendering because of cookies().
调用 cookies()headers()searchParams 等动态函数会使页面无法静态生成。
这是预期行为 — 这些函数依赖请求时信息,无法在构建时确定。如需部分静态:
1. 将动态部分隔离到单独的 Client Component
2. 使用 Suspense 包裹动态部分,让其余内容静态渲染

#34 revalidatePath 不生效

调用 revalidatePath 后页面仍显示旧内容
revalidatePath 只能在 Server Action 或 Route Handler 中调用。确保:
1. 调用位置正确(Server Action 内)
2. 路径参数与实际路由匹配
3. 使用 revalidatePath('/blog', 'layout') 可重新验证整个布局下的页面
4. 如果使用了 CDN 缓存,还需要清除 CDN 缓存

#35 Prisma 在 Server Component 中连接超限

Error: Too many database connections (P2037) - PrismaClientKnownRequestError
开发模式下热重载会创建多个 Prisma Client 实例,耗尽数据库连接池。
修复:使用全局单例模式:
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

#36 fetch 在 Server Action 中报错

Error: Dynamic server usage: Route /api/data couldn't be rendered statically because it used `cookies`.
API Route 或 Server Action 中使用了动态函数但被错误标记为静态。
修复:在 Route Handler 中显式声明动态:
// app/api/data/route.ts
export const dynamic = 'force-dynamic'

export async function GET() {
  const data = await getData()
  return Response.json(data)
}

#37 并行数据获取未生效

多个 fetch 请求串行执行导致页面加载缓慢
多个独立的数据请求应该并行执行:
// 错误:串行(慢)
const user = await getUser(id)
const posts = await getPosts(id)
const comments = await getComments(id)

// 正确:并行(快)
const [user, posts, comments] = await Promise.all([
  getUser(id),
  getPosts(id),
  getComments(id),
])

#38 unstable_cache 缓存不生效

使用 unstable_cache 包裹的函数每次都重新执行
unstable_cache(Next.js 14)/ use cache(Next.js 15)需要正确的缓存键和标签:
import { unstable_cache } from 'next/cache'

const getCachedPosts = unstable_cache(
  async () => {
    return db.post.findMany()
  },
  ['posts'],           // 缓存键
  { revalidate: 3600, tags: ['posts'] } // 选项
)

const posts = await getCachedPosts()
确保缓存键是唯一的且不包含动态值(除非作为参数传入)。

部署与生产问题 (#39 - #45)

#39 Vercel 部署失败 — 构建超时

Error: Build exceeded maximum allowed duration (45m).
项目过大或静态生成页面过多导致构建超时。
修复:
1. 减少 generateStaticParams 预生成页面数量,使用 dynamicParams 按需生成
2. 优化图片处理流程
3. 检查构建脚本中是否有不必要的操作
4. Vercel Pro 版本超时限制更高(45分钟 vs 免费版 10 分钟)

#40 环境变量在客户端不可用

TypeError: Cannot read properties of undefined (reading 'NEXT_PUBLIC_API_URL')
只有以 NEXT_PUBLIC_ 前缀开头的环境变量才会暴露给客户端:
# .env.local
DATABASE_URL=xxx           # 仅服务端可用
NEXT_PUBLIC_API_URL=xxx    # 客户端和服务端都可用
安全警告:永远不要将数据库密码、API 密钥等敏感信息放在 NEXT_PUBLIC_ 变量中!它们会被打包到客户端 JavaScript 中。

#41 Docker 部署后图片优化不可用

Error: 'sharp' is required for image optimization in production.
生产环境使用 next/image 优化需要安装 sharp
npm install sharp

# Docker Alpine 镜像中可能需要额外依赖
RUN apk add --no-cache libc6-compat
或在 next.config.js 中禁用图片优化:
module.exports = {
  images: { unoptimized: true }
}

#42 standalone 输出缺少静态文件

部署 standalone 输出后页面无样式、图片 404
output: 'standalone' 不会自动复制 public/.next/static/ 目录。需要手动复制:
# Dockerfile 中
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/static ./.next/static
或使用 CDN 托管静态资源并设置 assetPrefix

#43 ISR 在自托管部署中不生效

ISR 页面始终返回构建时的内容,不会重新生成
ISR 需要持久化的文件系统缓存。Serverless 或只读文件系统环境中不支持默认 ISR。
修复:
1. 确保部署环境文件系统可写
2. 使用 cacheHandler 自定义缓存后端(如 Redis):
// next.config.js
module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0, // 禁用内存缓存
}

3. 或使用 Vercel 部署(原生支持 ISR)

#44 CORS 错误 — API Route 跨域

Access to fetch at '/api/data' from origin 'http://localhost:3001' has been blocked by CORS policy.
Next.js API Route 默认不设置 CORS 头。需要手动添加:
// app/api/data/route.ts
export async function GET() {
  const data = await getData()
  return new Response(JSON.stringify(data), {
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  })
}

export async function OPTIONS() {
  return new Response(null, {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  })
}

#45 Vercel Edge Function 体积超限

Error: Edge Function size exceeds the 1 MB limit.
Middleware 和 Edge Runtime 的函数体积限制为 1MB(压缩后)。
修复:
1. 减少 Middleware 中导入的依赖
2. 不要在 Middleware 中导入大型库(如 ORM、heavy SDK)
3. 将复杂逻辑移到 Node.js Runtime 的 API Route 中
4. 使用 export const runtime = 'nodejs' 切换为 Node.js 运行时(放弃 Edge 优势)

性能与优化问题 (#46 - #50)

#46 next/image 图片加载失败

Error: Invalid src prop on `next/image`, hostname "cdn.example.com" is not configured under images in your `next.config.js`.
使用外部图片源时需要在 next.config.js 中配置允许的域名:
// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: '**.amazonaws.com',
      },
    ],
  },
}

#47 开发模式首次加载极慢

开发服务器首次编译耗时 30 秒以上
开发模式下 Next.js 按需编译页面,首次访问时会较慢。优化方法:
1. 使用 Turbopack:next dev --turbo(Next.js 15 默认启用)
2. 排除大型依赖的类型检查:在 tsconfig.json 中调整 skipLibCheck: true
3. 减少 app/layout.tsx 中导入的全局组件数量
4. 升级到最新版 Next.js 和 Node.js
5. 检查是否有过多的 barrel exports(index.ts 重新导出大量模块)

#48 Largest Contentful Paint (LCP) 指标差

Core Web Vitals 中 LCP 超过 2.5 秒
LCP 衡量页面最大内容元素的渲染时间。优化方法:
1. 对首屏大图使用 <Image priority /> 预加载
2. 使用 next/font 优化字体加载,避免 FOIT/FOUT
3. 减少首屏 Client Component 比例,尽量使用 Server Component
4. 启用 Streaming SSR,使内容尽早发送到浏览器
5. 使用 <Script strategy="lazyOnload" /> 延迟非关键脚本

#49 内存泄漏 — 生产环境 Node.js 进程 OOM

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
生产环境 Node.js 进程内存不断增长直到崩溃。
排查与修复:
1. 增加 Node.js 堆内存:NODE_OPTIONS='--max-old-space-size=4096'
2. 检查是否有全局变量累积数据(如全局缓存无上限)
3. 检查 Prisma/ORM 连接池配置
4. 使用 --inspect 标志和 Chrome DevTools 进行内存分析
5. 设置 PM2 或 Docker 内存限制并自动重启

#50 国际化(i18n)配置与路由冲突

App Router 中 i18n 配置被忽略或路由不正确
App Router 不支持 next.config.js 中的 i18n 配置(那是 Pages Router 的功能)。
App Router 中实现国际化的推荐方式:
1. 使用 [locale] 动态路由段:app/[locale]/page.tsx
2. 在 Middleware 中检测语言并重定向
3. 使用 next-intl 等社区库
// middleware.ts
import createMiddleware from 'next-intl/middleware'

export default createMiddleware({
  locales: ['zh', 'en', 'ja'],
  defaultLocale: 'zh',
})

export const config = {
  matcher: ['/((?!api|_next|.*\\..*).*)'],
}