Next.js 常见问题 FAQ(2025):App Router / Server Components / 渲染策略 50+ 问答

覆盖 Next.js 开发中最常遇到的问题:App Router vs Pages Router 选择、Server/Client Components 边界、数据获取与缓存、路由约定、Server Actions、性能优化、部署等 50+ 个高频问答,按主题组织,可即查即用。

本文是《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 RouterApp Router 替代方案
getStaticProps直接 fetch() + next: { revalidate }
getServerSideProps直接 fetch() + cache: 'no-store'
getStaticPathsgenerateStaticParams()
API Routes pages/api/*Route Handlers app/api/*/route.ts

App Router 的 page.tsxlayout.tsx 有什么区别?

  • page.tsx:路由的页面内容,只在该路由匹配时渲染
  • layout.tsx:包裹子路由的共享布局,在路由切换时保持不重新挂载(保留 state)
  • 一个路由可以有多个嵌套 layout:app/layout.tsxapp/blog/layout.tsxapp/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 等 HookClient Component
事件处理(onClick, onChange, onSubmitClient Component
浏览器 API(window, document, localStorageClient Component
第三方库需要客户端执行(如 framer-motion, three.jsClient 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 自动引用的资源路径不正确。检查:

  1. next.config.jsimages.unoptimized 是否被误设为 true?
  2. 浏览器请求的是正确的 /_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(第一字节时间)?

  1. 使用 ISR:缓存 HTML,避免每次请求都执行 Server Component
  2. 使用 Edge Runtimeexport const runtime = 'edge' 把渲染放到边缘节点
  3. 优化数据获取:用 Promise.all 并行获取多个数据源
  4. 减少中间件逻辑:Middleware 在每次请求时执行,保持精简

怎么减少 Bundle 大小?

  1. 动态导入const HeavyComponent = dynamic(() => import('./Heavy'))
  2. Tree shaking:确保只 import 需要的模块
  3. Client Component 最小化:把不需要交互的逻辑放在 Server Component
  4. 分析 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 部署后样式丢失?

检查:

  1. Tailwind CSS 配置:确保 content 包含了所有模板文件路径
  2. postcss.config.js 是否正确配置
  3. 生产构建:本地 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 的 useActionStateuseOptimisticuseFormStatus

'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 definedServer Component 用了浏览器 API"use client" 或条件判断 typeof window !== 'undefined'
fetch failedDNS/网络问题或服务器未启动检查 API URL 和服务可用性
PrismaClient is unable to run in this browser environmentPrisma 被 import 到 Client Component把数据库操作移到 Server Component 或 API Route
Dynamic server usage: headers/cookies静态页面生成时调用了动态 APIexport const dynamic = 'force-dynamic'
404 not found for dynamic routegenerateStaticParams 未返回该路径添加该路径到参数列表,或设 dynamicParams = true
Too many redirectsMiddleware / 路由配置循环跳转检查重定向条件是否相互触发
Module parse failed非 JS 文件未被 loader 处理配置 webpack 或 next.config.js 对应规则
Image with src "..." has invalid "width" property未声明外部图片尺寸添加 width/height 或使用 fill 模式
Invariant: cookies() expects to have requestAsyncStoragecookies() 在 static generation 时调用将页面设为动态:export const dynamic = 'force-dynamic'

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章