本文是《Next.js 完全指南》系列的 FAQ 补充,覆盖从入门到生产中最常遇到的 50+ 个问题。按主题分组,建议结合 Ctrl+F 快速定位。如果想系统学习,请参考 Next.js 完全指南专题。
一、App Router vs Pages Router
App Router 和 Pages Router 可以同时用吗?
可以兼容。同一个项目中可以同时存在 app/ 和 pages/ 目录,Next.js 会自动处理。但路由冲突时 App Router 优先(比如 app/about/page.tsx 会覆盖 pages/about.tsx)。新项目建议直接用 App Router,旧项目迁移建议逐步替换。
什么时候该从 Pages Router 迁移到 App Router?
- 新项目:直接选 App Router
- 现有项目:当需要 Server Components、Streaming、Nested Layouts 等特性时迁移
- Pages Router 不会被废弃,但新特性(如部分预渲染 PPR)只会优先在 App Router 上发布
App Router 下 getServerSideProps / getStaticProps 去哪了?
App Router 用完全不同的数据获取方式:
| Pages Router | App Router 替代方案 |
|---|---|
getStaticProps | 直接 fetch() + next: { revalidate } |
getServerSideProps | 直接 fetch() + cache: 'no-store' |
getStaticPaths | generateStaticParams() |
API Routes pages/api/* | Route Handlers app/api/*/route.ts |
App Router 的 page.tsx 和 layout.tsx 有什么区别?
page.tsx:路由的页面内容,只在该路由匹配时渲染layout.tsx:包裹子路由的共享布局,在路由切换时保持不重新挂载(保留 state)- 一个路由可以有多个嵌套 layout:
app/layout.tsx→app/blog/layout.tsx→app/blog/[slug]/page.tsx
App Router 的 loading.tsx 是怎么工作的?
当路由正在获取数据(内层的 async Server Component 还没 resolve)时,Next.js 自动渲染该路径的 loading.tsx。它本质上是 React Suspense 的 fallback 界面,数据准备好后自动替换为实际内容。
为什么我的 page.tsx 需要是 async 函数?
当 page.tsx 需要在服务端获取数据时(如直接 await db.findMany()),组件必须是 async 的。App Router 的 Server Components 支持 async 函数直接在服务端等待数据返回。
二、Server Components vs Client Components
怎么判断一个组件应该是 Server 还是 Client Component?
默认全用 Server Component,只有当需要以下能力时才加 "use client":
| 需要的能力 | 结论 |
|---|---|
useState, useEffect, useRef 等 Hook | Client Component |
事件处理(onClick, onChange, onSubmit) | Client Component |
浏览器 API(window, document, localStorage) | Client Component |
第三方库需要客户端执行(如 framer-motion, three.js) | Client Component |
| 直接从数据库/ORM 获取数据 | Server Component ✅ |
| 不需要交互的纯展示组件 | Server Component ✅ |
Server Component 里能 import Client Component 吗?
可以。反过来不行(Client Component 内不能 import Server Component)。典型模式:
// Server Component(page.tsx)
import { ProductCard } from './ProductCard'; // Client Component
export default async function Page() {
const products = await db.findMany(); // 服务端获取数据
return (
<div>
{products.map(p => (
<ProductCard key={p.id} product={p} /> // 传入数据给 Client Component
))}
</div>
);
}
Client Component 不能 import Server Component,那我该怎么传服务端数据给客户端?
通过 props 传递。Server Component 可以渲染 Client Component 并通过 props 传入服务端获取的数据:
// ClientComponent.tsx
'use client';
export function ClientComponent({ data }) {
const [count, setCount] = useState(0);
return <div>{data} - {count}</div>;
}
// ServerComponent.tsx
import { ClientComponent } from './ClientComponent';
export default async function ServerComponent() {
const data = await fetchData();
return <ClientComponent data={data} />; // ✅ 通过 props 传递
}
“use client” 组件的子组件需要再写 “use client” 吗?
不需要。父组件标记 "use client" 后,所有 import 和使用的子组件都自动在客户端执行。但建议把只在客户端使用的逻辑隔离到单独文件,保持组件职责清晰。
Server Component 能访问数据库吗?怎么在 Vercel 上连接?
直接在组件里调用 Prisma 或其他 ORM:
import { prisma } from '@/lib/prisma';
export default async function Page() {
const posts = await prisma.post.findMany();
return <BlogList posts={posts} />;
}
在 Vercel 上:把 DATABASE_URL 配置到环境变量即可,注意 Prisma 的 postinstall hook 里执行 prisma generate。
Server Component 有冷启动问题吗?
有。首次访问时,如果页面不是预生成的(ISR/SSG),Next.js 会执行 Server Component 渲染。在 Vercel 上这通过 Serverless Functions 实现,有冷启动(通常 50-500ms)。可以在 URL 上手动预热页面。
三、数据获取与缓存
App Router 里怎么实现 ISR?
// page.tsx
export const revalidate = 60; // 60 秒后台重新生成
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
});
// ...
}
或使用 revalidateTag 做按需重新验证:
fetch('/api/data', { next: { tags: ['posts'] } });
// 在 webhook/管理后台中调用 revalidateTag('posts')
fetch 的 cache 选项怎么选?
| 选项 | 行为 | 适用场景 |
|---|---|---|
{ cache: 'force-cache' } | 永久缓存(或基于 revalidate) | API 数据不常变 |
{ cache: 'no-store' } | 每次请求都重新获取 | 实时数据 |
{ next: { revalidate: 60 } } | ISR:缓存 60 秒后后台刷新 | 大部分内容页 |
{ next: { tags: ['users'] } } | 带标签缓存,支持按需失效 | 需要精确刷新 |
为什么我的 fetch 数据在开发时不更新?
Next.js 15 的 fetch 默认使用请求记忆(Request Memoization),在同一个渲染过程中,相同的 fetch 请求 URL 只会执行一次。开发环境下还有路由缓存,可能需要刷新页面才能看到新数据。添加 cache: 'no-store' 或 next: { revalidate: 0 } 禁用缓存。
Server Actions 里的表单数据怎么获取?
// Server Action
async function createPost(formData: FormData) {
'use server';
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await prisma.post.create({ data: { title, content } });
}
// 客户端表单
<form action={createPost}>
<input name="title" />
<textarea name="content" />
<button type="submit">Submit</button>
</form>
Server Actions 怎么返回错误给前端?
async function createPost(formData: FormData) {
'use server';
try {
// ... 数据验证
if (!title) return { error: 'Title is required' };
await prisma.post.create({ data });
return { success: true };
} catch (e) {
return { error: 'Database error' };
}
}
// 前端用 useFormState 接收
const [state, formAction] = useFormState(createPost, { error: null });
{state?.error && <div className="text-red-500">{state.error}</div>}
怎么在 Server Component 里获取请求信息(URL、Header、cookies)?
import { headers, cookies } from 'next/headers';
export default async function Page() {
const headersList = headers();
const userAgent = headersList.get('user-agent');
const cookieStore = cookies();
const token = cookieStore.get('token');
// 注意:这里不能获取 pathname 或 search params
// 动态路由参数通过 params prop 获取
}
四、路由与导航
_next/static 404 怎么办?
通常是 <Image> 组件或 Next.js 自动引用的资源路径不正确。检查:
next.config.js中images.unoptimized是否被误设为 true?- 浏览器请求的是正确的
/_next/static/路径吗?自定义服务器时可能需要配置静态文件路由。
怎么实现保护路由(未登录重定向)?
推荐用 Middleware:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
怎么实现多语言(i18n)?
App Router 推荐用路由段方案:
app/
├── [lang]/
│ ├── layout.tsx
│ ├── page.tsx
│ └── about/
│ └── page.tsx
// app/[lang]/layout.tsx
export default function RootLayout({ params, children }) {
const { lang } = params;
return <html lang={lang}>{children}</html>;
}
配合 Middleware 做默认语言重定向:
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
if (!pathname.startsWith('/en') && !pathname.startsWith('/zh')) {
return NextResponse.redirect(new URL(`/en${pathname}`, request.url));
}
}
怎么在 Next.js 里使用 hash 路由(如 #section-1)?
Next.js Router 不管理 hash,直接用标准浏览器 API:
'use client';
function scrollToSection(id: string) {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
}
// 或使用原生 <a href="#section-1"> 也可以
动态路由参数在 Server Component 里怎么获取?
// app/blog/[slug]/page.tsx
export default async function Page({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <PostContent post={post} />;
}
注意:params 是 promise-type(Next.js 15+),需要用 React.use() 解包或在 async 函数中 await:
export default async function Page(props: { params: Promise<{ slug: string }> }) {
const { slug } = await props.params;
}
五、性能优化
怎么减少 TTFB(第一字节时间)?
- 使用 ISR:缓存 HTML,避免每次请求都执行 Server Component
- 使用 Edge Runtime:
export const runtime = 'edge'把渲染放到边缘节点 - 优化数据获取:用
Promise.all并行获取多个数据源 - 减少中间件逻辑:Middleware 在每次请求时执行,保持精简
怎么减少 Bundle 大小?
- 动态导入:
const HeavyComponent = dynamic(() => import('./Heavy')) - Tree shaking:确保只 import 需要的模块
- Client Component 最小化:把不需要交互的逻辑放在 Server Component
- 分析 Bundle:
@next/bundle-analyzer
<Image> 组件尺寸警告怎么解决?
// 本地图片:推荐导入
import Image from 'next/image';
import myImage from './photo.jpg';
<Image src={myImage} alt="Photo" /> // 自动推断尺寸
// 外部图片:必须声明尺寸或 fill
<Image
src="https://example.com/photo.jpg"
alt="Photo"
width={800}
height={600}
// 或 fill + 父容器有 position: relative
/>
<Image> 懒加载不生效?
Next.js <Image> 默认开启懒加载(loading="lazy"),只有图片进入视口时才会加载。如果你需要立即加载:
<Image src={heroImage} alt="Hero" priority /> // 首屏图片加 priority
怎么监控 Core Web Vitals?
// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<SpeedInsights />
</body>
</html>
);
}
六、部署与生产
Vercel 部署后样式丢失?
检查:
- Tailwind CSS 配置:确保
content包含了所有模板文件路径 postcss.config.js是否正确配置- 生产构建:本地
npm run build后检查.next/static/css/是否有生成的 CSS
环境变量在服务端能读到但在客户端读不到?
Next.js 的环境变量规则:
| 前缀 | 服务端 | 客户端 |
|---|---|---|
NEXT_PUBLIC_ | ✅ | ✅ |
| 无前缀 | ✅ | ❌ |
客户端需要访问的环境变量必须用 NEXT_PUBLIC_ 前缀:
NEXT_PUBLIC_API_BASE=https://api.example.com # 客户端可用
DATABASE_URL=postgres://... # 仅服务端可用
Docker 部署后 API 路由 404?
检查 Dockerfile 是否正确暴露端口,以及 next.config.js 中的 output: 'standalone':
// next.config.js
module.exports = {
output: 'standalone', // 生成最小化独立运行包
};
Dockerfile 示例:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", ".next/standalone/server.js"]
怎么设置自定义域名?
Vercel:Dashboard → Project → Settings → Domains → 添加域名并配置 DNS CNAME。详见 Vercel 国内访问优化指南。
生产环境中 Prisma 连接数爆了?
Vercel 的 Serverless Functions 每个请求可能创建新的 Prisma 实例。用单例模式:
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
七、Next.js 15 新特性
after() API 是干什么的?
Next.js 15 引入的 unstable_after()(即将稳定),用于在响应发送后继续执行异步操作(如发送分析事件、记录日志),不会阻塞用户响应:
import { unstable_after as after } from 'next/server';
export default async function Page() {
const data = await fetchData();
after(async () => {
await logPageView(data.id);
});
return <div>{data.title}</div>;
}
Turbopack 能代替 Webpack 了吗?
Next.js 15 起 next dev 默认启用 Turbopack(Rust 编写的模块打包器),开发启动速度提升 10-50 倍。但 next build 仍使用 Webpack,正式构建正在向 Turbopack 迁移中。
React 19 的 Actions/Optimistic Updates 在 Next.js 里怎么用?
Next.js 15 已内置 React 19 的 useActionState、useOptimistic、useFormStatus:
'use client';
import { useOptimistic, useRef } from 'react';
function PostList({ posts }) {
const [optimisticPosts, addOptimisticPost] = useOptimistic(
posts,
(state, newPost) => [...state, newPost]
);
async function handleSubmit(formData: FormData) {
const title = formData.get('title');
addOptimisticPost({ id: 'temp', title, pending: true });
await createPost(formData); // Server Action
}
return (
<form action={handleSubmit}>
<input name="title" />
{optimisticPosts.map(p => (
<div key={p.id} style={{ opacity: p.pending ? 0.5 : 1 }}>{p.title}</div>
))}
</form>
);
}
八、错误排查速查表
| 错误信息 | 常见原因 | 修复 |
|---|---|---|
Cannot find module '@/...' | tsconfig paths 未配置 | 检查 paths: { "@/*": ["./*"] } |
window is not defined | Server Component 用了浏览器 API | 加 "use client" 或条件判断 typeof window !== 'undefined' |
fetch failed | DNS/网络问题或服务器未启动 | 检查 API URL 和服务可用性 |
PrismaClient is unable to run in this browser environment | Prisma 被 import 到 Client Component | 把数据库操作移到 Server Component 或 API Route |
Dynamic server usage: headers/cookies | 静态页面生成时调用了动态 API | 加 export const dynamic = 'force-dynamic' |
404 not found for dynamic route | generateStaticParams 未返回该路径 | 添加该路径到参数列表,或设 dynamicParams = true |
Too many redirects | Middleware / 路由配置循环跳转 | 检查重定向条件是否相互触发 |
Module parse failed | 非 JS 文件未被 loader 处理 | 配置 webpack 或 next.config.js 对应规则 |
Image with src "..." has invalid "width" property | 未声明外部图片尺寸 | 添加 width/height 或使用 fill 模式 |
Invariant: cookies() expects to have requestAsyncStorage | cookies() 在 static generation 时调用 | 将页面设为动态:export const dynamic = 'force-dynamic' |
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。