切换语言
切换主题

Next.js App Router 常见踩坑与解决方案:8个让你少走弯路的实战经验

引言

说实话,刚开始用 Next.js App Router 的时候,我真的被坑惨了。

去年年底公司项目要升级到 Next.js 15,我想着既然都升级了,不如顺便把 Pages Router 迁移到 App Router,毕竟官方文档吹得那么好:“更快的性能”、“更好的开发体验”、“Server Components 革命性架构”。结果呢?上手第一天就遇到了一堆诡异问题。

数据不更新、页面一直转圈、明明写了缓存却不生效、Server Component 和 Client Component 傻傻分不清… 最夸张的是,有个 bug 我整整 debug 了 3 个小时,最后发现是因为 error.tsx 文件里忘了加 'use client'。当时那个心情,真是欲哭无泪。

后来在团队内部做了个统计,发现大家遇到的问题有 80% 都是重复的。于是我就把这些踩过的坑整理出来,希望能帮你少走点弯路。

这篇文章不讲理论,只讲实战经验。每个问题都会告诉你:为什么会踩坑、怎么发现的、怎么解决的。看完你就知道 App Router 的那些”坑”该怎么避开了。

数据获取的那些坑

坑 1:在客户端重复获取数据

场景还原

有一次我需要在页面上显示用户信息,习惯性地写了这样的代码:

// app/profile/page.tsx
'use client'
import { useEffect, useState } from 'react'

export default function ProfilePage() {
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => setUser(data))
  }, [])

  if (!user) return <div>Loading...</div>
  return <div>Hello, {user.name}</div>
}

看起来没问题对吧?但实际上这是个典型的反模式。数据从数据库 → API Route → 客户端,多了一次不必要的网络往返。

为什么会踩坑

Pages Router 时代我们习惯了用 useEffect 在客户端获取数据,但 App Router 的 Server Components 可以直接在服务端拿数据,根本不需要多这一层 API。

正确的做法

// app/profile/page.tsx(默认是 Server Component)
import { db } from '@/lib/db'

export default async function ProfilePage() {
  // 直接在服务端查数据库
  const user = await db.user.findFirst()

  return <div>Hello, {user.name}</div>
}

性能提升立竿见影:

  • 少了一次 API 请求
  • 服务端到数据库的延迟通常低于 10ms(客户端到服务端可能 100ms+)
  • 减少了客户端 JavaScript bundle 大小

划重点

能在 Server Component 里拿的数据,就别跑到客户端去 fetch。只有需要用户交互(搜索、筛选、实时更新)时才考虑客户端获取。

坑 2:Route Handler 的默认缓存行为

场景还原

我写了个 API 返回当前时间,结果发现无论怎么刷新,时间都不变:

// app/api/time/route.ts
export async function GET() {
  return Response.json({ time: new Date().toISOString() })
}

刷新 10 次,返回的时间一模一样。我当时都怀疑是不是代码没生效。

为什么会踩坑

Next.js 默认会缓存 GET 请求的 Route Handler,这对静态数据(如配置信息)是好事,但对动态数据就麻烦了。

解决方案 1:明确标记为动态

// app/api/time/route.ts
export const dynamic = 'force-dynamic' // 强制动态渲染

export async function GET() {
  return Response.json({ time: new Date().toISOString() })
}

解决方案 2:使用 Next.js 15 的新默认行为

好消息是,Next.js 15 已经把 GET Route Handler 的默认行为改为不缓存了。如果你还在用 Next.js 14,可以这样写:

// app/api/time/route.ts
export async function GET() {
  return Response.json(
    { time: new Date().toISOString() },
    { headers: { 'Cache-Control': 'no-store' } }
  )
}

我的经验

现在我的习惯是:

  • 静态数据(配置、常量):明确标记 export const revalidate = 3600
  • 动态数据(用户信息、实时数据):标记 export const dynamic = 'force-dynamic'

别依赖默认行为,明确你的意图,代码更清晰。

坑 3:数据变更后忘记重新验证

场景还原

做了个简单的 Todo 应用,添加新任务后列表不更新:

// app/todos/page.tsx
export default async function TodosPage() {
  const todos = await db.todo.findMany()
  return <TodoList todos={todos} />
}

// app/actions.ts
'use server'
export async function addTodo(text: string) {
  await db.todo.create({ data: { text } })
  // 忘了重新验证!
}

提交表单后页面还是显示旧数据,必须手动刷新才能看到新任务。

为什么会踩坑

App Router 的缓存机制很激进,即使数据变了,页面也不会自动更新,需要你明确告诉它”这个路径的数据变了,快去刷新”。

正确的做法

// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'

export async function addTodo(text: string) {
  await db.todo.create({ data: { text } })
  revalidatePath('/todos') // 重新验证 /todos 路径
}

进阶技巧

如果多个页面都显示 Todo 列表(如首页、归档页),用 revalidateTag 更灵活:

// app/todos/page.tsx
export default async function TodosPage() {
  const todos = await fetch('http://localhost:3000/api/todos', {
    next: { tags: ['todos'] } // 给数据打标签
  })
  return <TodoList todos={todos} />
}

// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function addTodo(text: string) {
  await db.todo.create({ data: { text } })
  revalidateTag('todos') // 重新验证所有带 'todos' 标签的数据
}

划重点

数据变更三件套:写入数据 → revalidatePath / revalidateTag → 重定向(可选)

Server Components 和 Client Components 的迷惑行为

坑 4:在 Server Component 里用 Context

场景还原

我想在全局提供主题切换功能,于是写了个 ThemeProvider:

// app/providers.tsx
import { createContext } from 'react'

export const ThemeContext = createContext('light')

export function Providers({ children }) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  )
}

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

结果报错:You're importing a component that needs createContext. This only works in a Client Component.

为什么会踩坑

Server Components 不支持 React Context,因为它们在服务端渲染,没有客户端的状态管理机制。

正确的做法

Provider 必须是 Client Component,而且要单独提取出来:

// app/providers.tsx
'use client' // 关键:标记为 Client Component

import { createContext, useState } from 'react'

export const ThemeContext = createContext('light')

export function Providers({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light')

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// app/layout.tsx(还是 Server Component)
import { Providers } from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

我踩过的坑

一开始我把 'use client' 加到了 layout.tsx 里,结果整个应用都变成 Client Component 了,Server Components 的优势全没了。记住:只把 Provider 标记为 Client Component,Layout 保持 Server Component

坑 5:Client Component 的 SSR 误解

场景还原

我在 Client Component 里用了 localStorage,本地开发没问题,部署后报错:localStorage is not defined

// app/components/user-info.tsx
'use client'

export default function UserInfo() {
  const user = JSON.parse(localStorage.getItem('user') || '{}')
  return <div>{user.name}</div>
}

为什么会踩坑

很多人以为 'use client' 就意味着”只在客户端运行”,但其实 Client Components 在服务端也会预渲染(SSR)。localStorage 只存在于浏览器环境,服务端访问它当然会报错。

解决方案 1:检查环境

'use client'
import { useEffect, useState } from 'react'

export default function UserInfo() {
  const [user, setUser] = useState(null)

  useEffect(() => {
    // useEffect 只在客户端执行
    const userData = JSON.parse(localStorage.getItem('user') || '{}')
    setUser(userData)
  }, [])

  if (!user) return null
  return <div>{user.name}</div>
}

解决方案 2:使用条件判断

'use client'

export default function UserInfo() {
  const user = typeof window !== 'undefined'
    ? JSON.parse(localStorage.getItem('user') || '{}')
    : null

  if (!user) return null
  return <div>{user.name}</div>
}

划重点

Client Component = 可以在客户端交互的组件,但它依然会在服务端预渲染。涉及浏览器 API(localStorage、window、document)的代码必须放在 useEffect 里或加环境判断。

坑 6:过度使用 ‘use client’

场景还原

刚开始用 App Router 时,我遇到报错就习惯性地加 'use client',结果最后发现整个项目几乎都是 Client Components,Server Components 的优势荡然无存。

为什么会踩坑

有些问题看起来像是”需要 Client Component”,其实只是代码组织问题。

反例

// app/dashboard/page.tsx
'use client' // 不应该!

import { useState } from 'react'

export default function Dashboard() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <Header /> {/* 静态的 */}
      <Stats /> {/* 需要服务端数据 */}
      <Counter count={count} setCount={setCount} /> {/* 需要交互 */}
    </div>
  )
}

这样写的话,整个页面都变成 Client Component,Stats 的数据也要走客户端 fetch。

正确做法

// app/dashboard/page.tsx(Server Component)
import { db } from '@/lib/db'
import { Counter } from './counter'

export default async function Dashboard() {
  const stats = await db.stats.findFirst() // 服务端获取数据

  return (
    <div>
      <Header /> {/* Server Component */}
      <Stats data={stats} /> {/* Server Component */}
      <Counter /> {/* Client Component */}
    </div>
  )
}

// app/dashboard/counter.tsx
'use client' // 只有这个组件是 Client Component

import { useState } from 'react'

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

我的经验

判断一个组件是否需要 'use client' 的三个标准:

  1. 用了 React Hooks(useState、useEffect、useContext 等)
  2. 需要监听浏览器事件(onClick、onChange 等)
  3. 用了浏览器 API(localStorage、window 等)

只要不符合这三条,就保持 Server Component。

缓存机制的坑

坑 7:Client Router Cache 的困惑

场景还原

用户在 /posts/1 页面编辑了文章,保存后跳转到 /posts 列表页,但列表里的文章标题还是旧的。刷新页面才能看到更新。

为什么会踩坑

App Router 有个 Client Router Cache(客户端路由缓存),它会缓存你访问过的页面,即使数据更新了,跳转时还是会显示缓存的旧数据。

解决方案 1:跳转时刷新

// app/posts/[id]/edit/page.tsx
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'

export async function updatePost(id: string, title: string) {
  await db.post.update({ where: { id }, data: { title } })

  revalidatePath('/posts') // 重新验证列表页
  revalidatePath(`/posts/${id}`) // 重新验证详情页

  redirect('/posts') // 跳转回列表
}

解决方案 2:使用 router.refresh()

'use client'
import { useRouter } from 'next/navigation'

export function EditForm() {
  const router = useRouter()

  async function handleSubmit() {
    await updatePost(...)
    router.refresh() // 刷新当前路由的数据
    router.push('/posts')
  }
}

Next.js 15 的好消息

Next.js 15 把 Client Router Cache 的默认行为改为不缓存了,这个问题基本不用担心了。但如果你用的是 Next.js 14,记得手动处理。

坑 8:revalidate 不生效

场景还原

我在 Server Component 里设置了 revalidate = 60,期望每 60 秒自动刷新数据,但实际上数据一直不变。

// app/news/page.tsx
export const revalidate = 60 // 期望 60 秒后重新生成

export default async function NewsPage() {
  const news = await fetch('https://api.example.com/news')
  return <NewsList news={news} />
}

部署后发现新闻列表一整天都不更新。

为什么会踩坑

revalidate 只在生产环境生效,开发环境(npm run dev)不会缓存。另外,它只对静态生成的页面有效,如果页面被识别为动态渲染,revalidate 就失效了。

排查步骤

  1. 确认是生产环境
npm run build
npm run start
  1. 检查页面是否是静态的

构建时看输出,应该是 ○ Static● SSG 标记。如果是 λ Dynamic,说明页面被识别为动态渲染了。

  1. 找出导致动态渲染的原因

常见原因:

  • 用了 cookies()headers()
  • 用了 searchParams(动态路由参数)
  • Route Handler 没有明确设置 revalidate

解决方案

// app/news/page.tsx
export const revalidate = 60

export default async function NewsPage() {
  const news = await fetch('https://api.example.com/news', {
    next: { revalidate: 60 } // fetch 级别的 revalidate
  })

  return <NewsList news={news} />
}

我的经验

现在我的习惯是:

  • 纯静态内容:用 generateStaticParams + revalidate
  • 需要动态参数:用 ISR(Incremental Static Regeneration)
  • 实时数据:直接标记 dynamic = 'force-dynamic',别用 revalidate

错误处理的坑

坑 9:error.tsx 忘记加 ‘use client’

场景还原

我创建了 error.tsx 来处理页面错误,结果报错:ReactServerComponentsError: Client Component must be used in a Client Component boundary.

// app/error.tsx(错误写法)
export default function Error({ error, reset }) {
  return (
    <div>
      <h2>出错了!</h2>
      <button onClick={reset}>重试</button>
    </div>
  )
}

为什么会踩坑

error.tsx 必须是 Client Component,因为它需要 React 的 Error Boundary 机制,而 Error Boundary 只在客户端有效。

正确写法

// app/error.tsx
'use client' // 必须加这个

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

划重点

error.tsxloading.tsxnot-found.tsx 这些特殊文件,只有 error.tsx 必须是 Client Component,其他可以是 Server Component。

坑 10:redirect 在 try/catch 中的位置问题

场景还原

我在 Server Action 里做表单验证,验证失败后想跳转到错误页,但 redirect 被 catch 捕获了,导致跳转失败。

// app/actions.ts(错误写法)
'use server'
import { redirect } from 'next/navigation'

export async function createUser(data: FormData) {
  try {
    const user = await db.user.create({ data })
    redirect(`/users/${user.id}`) // 这里会被 catch 捕获!
  } catch (error) {
    console.error(error)
    return { error: 'Failed to create user' }
  }
}

为什么会踩坑

redirect() 的实现原理是抛出一个特殊的错误,Next.js 会捕获这个错误并执行跳转。如果你在 try/catch 里用 redirect,它就会被你的 catch 捕获,导致跳转失败。

正确写法

// app/actions.ts
'use server'
import { redirect } from 'next/navigation'

export async function createUser(data: FormData) {
  try {
    const user = await db.user.create({ data })
    // 不在这里 redirect
    return { success: true, userId: user.id }
  } catch (error) {
    console.error(error)
    return { error: 'Failed to create user' }
  }
}

// 在调用处 redirect
export async function handleSubmit(data: FormData) {
  const result = await createUser(data)
  if (result.success) {
    redirect(`/users/${result.userId}`) // 在 try/catch 外面
  }
}

或者这样写

'use server'
import { redirect } from 'next/navigation'

export async function createUser(data: FormData) {
  try {
    const user = await db.user.create({ data })
  } catch (error) {
    console.error(error)
    return { error: 'Failed to create user' }
  }

  redirect(`/users/${user.id}`) // 在 try/catch 后面
}

迁移时的坑

坑 11:404.js 和 500.js 不能用了

场景还原

从 Pages Router 迁移时,我保留了 pages/404.jspages/500.js,结果发现这两个页面一直不生效。

为什么会踩坑

App Router 的错误处理机制完全变了:

  • 404.jsnot-found.tsx
  • 500.jserror.tsx
  • 全局错误 → global-error.tsx

正确做法

// app/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h2>404 - 页面不存在</h2>
      <Link href="/">回到首页</Link>
    </div>
  )
}

// app/error.tsx
'use client'

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>500 - 服务器错误</h2>
      <p>{error.message}</p>
      <button onClick={reset}>重试</button>
    </div>
  )
}

// app/global-error.tsx(捕获根布局的错误)
'use client'

export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <h2>全局错误</h2>
        <p>{error.message}</p>
        <button onClick={reset}>重试</button>
      </body>
    </html>
  )
}

坑 12:next-seo 不能用了

场景还原

我的项目大量使用了 next-seo 来管理 SEO 元数据,迁移到 App Router 后发现不生效了。

// pages/blog/[slug].tsx(Pages Router 时代)
import { NextSeo } from 'next-seo'

export default function BlogPost({ post }) {
  return (
    <>
      <NextSeo
        title={post.title}
        description={post.excerpt}
        openGraph={{
          title: post.title,
          description: post.excerpt,
          images: [{ url: post.coverImage }],
        }}
      />
      <article>{post.content}</article>
    </>
  )
}

为什么会踩坑

App Router 有了原生的 generateMetadata API,next-seo 已经不推荐使用了。

迁移方案

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'

export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  }
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)
  return <article>{post.content}</article>
}

好处:

  1. 类型安全(TypeScript 支持)
  2. 支持 async/await(可以直接查数据库)
  3. 更好的性能(服务端渲染)

性能优化建议

避免不必要的 Client Components

问题:整个页面都变成 Client Component,失去了 Server Components 的优势。

解决方案

采用”叶子节点 Client Components”策略:

// ❌ 不好的做法
// app/dashboard/page.tsx
'use client'
export default function Dashboard() {
  return (
    <div>
      <Header />
      <Sidebar />
      <MainContent />
      <Footer />
    </div>
  )
}

// ✅ 好的做法
// app/dashboard/page.tsx(Server Component)
import { Header } from './header'
import { Sidebar } from './sidebar'
import { MainContent } from './main-content'
import { Footer } from './footer'

export default function Dashboard() {
  return (
    <div>
      <Header /> {/* Server Component */}
      <Sidebar /> {/* Client Component(交互) */}
      <MainContent /> {/* Server Component */}
      <Footer /> {/* Server Component */}
    </div>
  )
}

// app/dashboard/sidebar.tsx
'use client' // 只有这个是 Client Component
export function Sidebar() {
  const [collapsed, setCollapsed] = useState(false)
  return <aside>...</aside>
}

优化 Suspense 边界

问题:整个页面都在等待慢速数据,导致首屏白屏时间长。

解决方案

<Suspense> 拆分加载:

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { FastComponent } from './fast'
import { SlowComponent } from './slow'

export default function Dashboard() {
  return (
    <div>
      {/* 快速数据立即显示 */}
      <FastComponent />

      {/* 慢速数据显示骨架屏 */}
      <Suspense fallback={<div>加载中...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}

合理使用并行数据获取

问题:串行获取数据,总耗时 = 所有请求时间之和。

解决方案

// ❌ 串行获取(慢)
export default async function Page() {
  const user = await getUser() // 100ms
  const posts = await getPosts() // 200ms
  const comments = await getComments() // 150ms
  // 总耗时:450ms
}

// ✅ 并行获取(快)
export default async function Page() {
  const [user, posts, comments] = await Promise.all([
    getUser(),
    getPosts(),
    getComments(),
  ])
  // 总耗时:200ms(最慢的那个)
}

开发环境的坑

坑 13:热重载导致的连接泄漏

场景还原

开发环境跑了一会儿,数据库报错:too many connections

为什么会踩坑

热重载(Hot Reload)会重新执行模块代码,如果你在全局创建数据库连接,每次热重载都会创建新连接,旧连接不会关闭。

解决方案

// lib/db.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = global as unknown as { prisma: PrismaClient }

export const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    log: ['query'],
  })

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

这样在开发环境下,热重载时会复用同一个 Prisma 实例。

坑 14:开发服务器越来越慢

场景还原

npm run dev 跑了半小时后,热重载变得特别慢,有时候甚至卡死。

为什么会踩坑

App Router 的开发服务器在处理大量页面时会占用大量内存,尤其是有很多动态路由时。

临时解决方案

  1. 重启开发服务器(治标不治本)
  2. 减少不必要的文件监听:
// next.config.js
module.exports = {
  webpack: (config) => {
    config.watchOptions = {
      poll: 1000, // 每秒检查一次文件变化(降低频率)
      aggregateTimeout: 300,
      ignored: /node_modules/,
    }
    return config
  },
}

长期解决方案

升级到 Next.js 15,使用 Turbopack:

npm run dev --turbo

Turbopack 的热重载速度快 10 倍以上,大项目也不会卡顿。

总结:避坑清单

看了这么多坑,我整理了一个快速检查清单,新项目开始前过一遍,能避免 90% 的问题:

数据获取

  • ☑ 能用 Server Component 获取数据,就不要用 Client Component + useEffect
  • ☑ Route Handler 明确标记 dynamic = 'force-dynamic'revalidate
  • ☑ 数据变更后记得 revalidatePath / revalidateTag

Server/Client Components

  • ☑ Provider 必须是 Client Component,但 Layout 保持 Server Component
  • ☑ 浏览器 API(localStorage、window)必须在 useEffect 里或加环境判断
  • ☑ 只给真正需要交互的组件加 'use client',不要”图方便”整个页面都加

错误处理

  • error.tsx 必须加 'use client'
  • redirect 不要放在 try/catch 里面
  • ☑ 根布局错误用 global-error.tsx,不是 error.tsx

缓存机制

  • ☑ 升级到 Next.js 15,享受更合理的默认缓存行为
  • ☑ revalidate 只在生产环境生效,开发环境不要依赖它
  • ☑ 动态页面别用 revalidate,直接 dynamic = 'force-dynamic'

迁移相关

  • 404.jsnot-found.tsx500.jserror.tsx
  • next-seogenerateMetadata
  • getServerSideProps → Server Component 直接 fetch
  • useRouternext/routernext/navigation

性能优化

  • ☑ 用 Suspense 拆分快慢组件
  • ☑ 并行获取数据(Promise.all
  • ☑ 数据库连接在开发环境使用单例模式
  • ☑ 使用 Turbopack(npm run dev --turbo

写在最后

说实话,App Router 确实有学习曲线,刚开始踩坑是正常的。但一旦掌握了这些”套路”,开发效率真的会提升不少。

我现在的习惯是:

  1. 新功能先思考数据流:这个数据需要服务端渲染还是客户端交互?
  2. 遇到问题先看构建输出:页面是 Static 还是 Dynamic?为什么?
  3. 善用 DevTools:Network 面板看请求数、Console 看错误堆栈
  4. 不要依赖默认行为:明确你的意图(缓存、渲染方式、数据重新验证)

最重要的一点:别被这些坑吓到了,动手试一试,每个坑踩过一次就记住了。Next.js App Router 的官方文档写得很详细,遇到问题多翻翻文档,答案基本都在里面。

如果这篇文章帮到了你,欢迎分享给正在踩坑的朋友。有新的坑欢迎在评论区补充,我会持续更新这个列表。

祝你在 App Router 的道路上少走弯路,多写 bug(划掉),多写优雅的代码!

常见问题

Server Component 和 Client Component 怎么区分?
Server Component(默认):
• 在服务端运行,不发送到客户端
• 不能使用useState、useEffect等hooks
• 不能使用浏览器API

Client Component(需要标记):
• 使用'use client'指令标记
• 可以使用所有React hooks
• 可以使用浏览器API

判断标准:如果需要交互或浏览器API,就用Client Component
为什么数据不更新?
Next.js的fetch默认有缓存。

解决方案:
• 设置cache: 'no-store'(每次请求都拉新数据)
• 使用next: { revalidate: 60 }(60秒后重新验证)
• 在Client Component中使用router.refresh()强制刷新

检查方法:查看构建输出,确认页面是Dynamic还是Static
页面一直转圈怎么办?
可能原因:
• async Server Component没有正确处理loading状态
• Suspense边界配置错误
• 数据获取失败但没有错误处理

解决方法:
• 添加loading.tsx文件
• 使用Suspense包裹异步组件
• 添加error.tsx处理错误
error.tsx 不生效?
error.tsx必须是Client Component。

必须添加'use client'指令:
'use client'

export default function Error({ error, reset }) {
return <div>出错了:{error.message}</div>
}

注意:error.tsx只能捕获子组件的错误,不能捕获自己的错误
如何从 Pages Router 迁移到 App Router?
主要变化:
• getServerSideProps → async Server Component
• getStaticProps → 静态生成(默认)
• next/router → next/navigation
• _app.js → layout.tsx
• _document.js → 不需要(layout.tsx处理)

建议:先选1-2个页面试点,跑通流程后再全面推进
缓存机制怎么理解?
Next.js的缓存层级:
• Request Memoization:同一次请求中相同fetch只执行一次
• Data Cache:fetch的响应会被缓存
• Full Route Cache:整个页面会被缓存(静态生成)
• Router Cache:客户端路由缓存

控制方法:使用cache: 'no-store'、next: { revalidate }等选项
如何调试 App Router 问题?
调试方法:
• 查看构建输出(npm run build)确认页面类型
• 使用DevTools Network面板查看请求
• 检查Console错误信息
• 查看Next.js终端输出

常见问题:
• 页面是Static但应该是Dynamic → 检查fetch缓存配置
• 数据不更新 → 检查缓存和revalidate设置
• 页面转圈 → 检查loading.tsx和Suspense

12 分钟阅读 · 发布于: 2025年12月25日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章