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' 的三个标准:
- 用了 React Hooks(useState、useEffect、useContext 等)
- 需要监听浏览器事件(onClick、onChange 等)
- 用了浏览器 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 就失效了。
排查步骤:
- 确认是生产环境:
npm run build
npm run start- 检查页面是否是静态的:
构建时看输出,应该是 ○ Static 或 ● SSG 标记。如果是 λ Dynamic,说明页面被识别为动态渲染了。
- 找出导致动态渲染的原因:
常见原因:
- 用了
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.tsx、loading.tsx、not-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.js 和 pages/500.js,结果发现这两个页面一直不生效。
为什么会踩坑:
App Router 的错误处理机制完全变了:
404.js→not-found.tsx500.js→error.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>
}好处:
- 类型安全(TypeScript 支持)
- 支持 async/await(可以直接查数据库)
- 更好的性能(服务端渲染)
性能优化建议
避免不必要的 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 的开发服务器在处理大量页面时会占用大量内存,尤其是有很多动态路由时。
临时解决方案:
- 重启开发服务器(治标不治本)
- 减少不必要的文件监听:
// 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 --turboTurbopack 的热重载速度快 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.js→not-found.tsx,500.js→error.tsx - ☑
next-seo→generateMetadata - ☑
getServerSideProps→ Server Component 直接 fetch - ☑
useRouter从next/router→next/navigation
性能优化
- ☑ 用 Suspense 拆分快慢组件
- ☑ 并行获取数据(
Promise.all) - ☑ 数据库连接在开发环境使用单例模式
- ☑ 使用 Turbopack(
npm run dev --turbo)
写在最后
说实话,App Router 确实有学习曲线,刚开始踩坑是正常的。但一旦掌握了这些”套路”,开发效率真的会提升不少。
我现在的习惯是:
- 新功能先思考数据流:这个数据需要服务端渲染还是客户端交互?
- 遇到问题先看构建输出:页面是 Static 还是 Dynamic?为什么?
- 善用 DevTools:Network 面板看请求数、Console 看错误堆栈
- 不要依赖默认行为:明确你的意图(缓存、渲染方式、数据重新验证)
最重要的一点:别被这些坑吓到了,动手试一试,每个坑踩过一次就记住了。Next.js App Router 的官方文档写得很详细,遇到问题多翻翻文档,答案基本都在里面。
如果这篇文章帮到了你,欢迎分享给正在踩坑的朋友。有新的坑欢迎在评论区补充,我会持续更新这个列表。
祝你在 App Router 的道路上少走弯路,多写 bug(划掉),多写优雅的代码!
常见问题
Server Component 和 Client Component 怎么区分?
• 在服务端运行,不发送到客户端
• 不能使用useState、useEffect等hooks
• 不能使用浏览器API
Client Component(需要标记):
• 使用'use client'指令标记
• 可以使用所有React hooks
• 可以使用浏览器API
判断标准:如果需要交互或浏览器API,就用Client Component
为什么数据不更新?
解决方案:
• 设置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 不生效?
必须添加'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个页面试点,跑通流程后再全面推进
缓存机制怎么理解?
• 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日
相关文章
Next.js 电商实战:购物车与 Stripe 支付完整实现指南

Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战

Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南


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