跳到主要内容

Next.js 核心知识

问题

什么是 Next.js?它解决了哪些 React 开发中的痛点?App Router、Server Components、缓存机制等核心概念如何理解?

答案

Next.js 是 Vercel 推出的基于 React 的全栈框架,提供了 SSR/SSG/ISR 等多种渲染模式、文件系统路由、API Routes、Server Components 等开箱即用的能力。它极大地简化了 React 应用的开发和部署流程,是目前 React 官方推荐的生产级框架。

核心概念总览


1. Next.js 概述

为什么选择 Next.js

能力纯 React (CRA)Next.js
渲染模式仅 CSRCSR / SSR / SSG / ISR / Streaming
路由需要 react-router文件系统路由,开箱即用
SEO差(空 HTML)好(服务端预渲染)
首屏性能慢(大 JS bundle)快(Server Components + Streaming)
API 接口需要单独后端Route Handlers / Server Actions
代码分割手动配置自动按路由分割
图片优化需要手动处理next/image 自动优化
部署需要静态托管Vercel 一键部署 / Docker / 静态导出
关键点

Next.js 的核心价值在于:将原本需要大量配置才能实现的 SSR、代码分割、图片优化等能力以零配置方式提供,让开发者专注于业务逻辑。

Next.js 版本演进

版本重要特性
Next.js 12Middleware、SWC 编译器、ISR 改进
Next.js 13App Router(beta)、Server Components、Turbopack
Next.js 14App Router 稳定、Server Actions 稳定、Partial Prerendering(preview)
Next.js 15React 19 支持、Turbopack Dev 稳定、异步请求 API

2. 渲染模式

Next.js 支持多种渲染模式,每种模式适用于不同的场景。关于 SSR/SSG 的更多细节可参考 SSR 与 SSG首屏优化

渲染流程对比

模式对比表

特性CSRSSRSSGISR
渲染时机运行时(浏览器)运行时(服务器)构建时构建时 + 按需更新
首屏速度最快
SEO
数据时效性实时实时构建时可配置(秒级)
服务器压力
适用场景后台管理系统个性化内容博客/文档电商/新闻

App Router 中的实现

在 App Router 中,渲染模式通过组件类型和 fetch 配置来控制:

app/posts/page.tsx
// SSG:默认行为,构建时获取数据并缓存
async function StaticPage() {
const data = await fetch('https://api.example.com/posts');
const posts = await data.json();
return <PostList posts={posts} />;
}

// SSR:每次请求都重新获取数据
async function DynamicPage() {
const data = await fetch('https://api.example.com/posts', { cache: 'no-store' });
const posts = await data.json();
return <PostList posts={posts} />;
}

// ISR:缓存并在 60 秒后 revalidate
async function ISRPage() {
const data = await fetch('https://api.example.com/posts', { next: { revalidate: 60 } });
const posts = await data.json();
return <PostList posts={posts} />;
}

export default StaticPage;
补充

在 App Router 中,Next.js 会自动根据组件是否使用动态 API(如 cookies()headers()searchParams)来决定是 Static 还是 Dynamic 渲染,无需手动声明 getServerSidePropsgetStaticProps


3. App Router vs Pages Router

Next.js 13 引入了 App Router,这是对路由系统的一次重大重构。它基于 React Server Components、嵌套布局和 React 18 的 Suspense 等新特性。

核心区别

对比项Pages RouterApp Router
目录pages/app/
路由文件任意文件名即路由必须使用 page.tsx
布局_app.tsx + _document.tsx(全局)layout.tsx(嵌套布局)
数据获取getServerSideProps / getStaticPropsasync Server Components / fetch
Server Components不支持默认支持
Streaming不支持原生支持
加载状态手动实现loading.tsx 约定
错误处理_error.tsx(全局)error.tsx(嵌套)
Middleware支持支持
APIpages/api/app/api/route.ts + Server Actions
状态稳定(维护模式)稳定(推荐)

App Router 文件约定

app/
├── layout.tsx # 根布局(必须)
├── page.tsx # 首页 /
├── loading.tsx # 全局 Loading UI
├── error.tsx # 全局 Error UI
├── not-found.tsx # 404 页面
├── global-error.tsx # 根错误边界
├── blog/
│ ├── layout.tsx # 博客嵌套布局
│ ├── page.tsx # /blog
│ ├── loading.tsx # 博客加载状态
│ ├── error.tsx # 博客错误边界
│ └── [slug]/
│ └── page.tsx # /blog/:slug
├── (marketing)/ # 路由组(不影响 URL)
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
└── api/
└── posts/
└── route.ts # API Route Handler

嵌套布局

嵌套布局是 App Router 最重要的特性之一,它允许在不同路由层级共享 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
// 仪表盘布局:仅 /dashboard/* 页面使用
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<aside>侧边栏菜单</aside>
<main>{children}</main>
</div>
);
}
关键点

布局组件在导航时不会重新渲染,只有 page.tsx 会更新。这意味着布局中的状态会在页面切换时保持,极大提升了导航性能。

loading.tsx 与 error.tsx

app/dashboard/loading.tsx
// 自动包裹在 Suspense 中,作为 fallback
export default function Loading() {
return <div className="skeleton">加载中...</div>;
}
app/dashboard/error.tsx
'use client'; // Error 组件必须是 Client Component

import { useEffect } from 'react';

export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);

return (
<div>
<h2>出错了!</h2>
<button onClick={() => reset()}>重试</button>
</div>
);
}

4. Server Components (RSC)

React Server Components 是 React 19 正式引入的新范式(参考 React 19 新特性),而 Next.js 是第一个全面集成 RSC 的框架。

RSC 工作原理

Server Components vs Client Components

对比项Server ComponentsClient Components
默认行为App Router 中默认需要 'use client' 声明
执行环境仅服务端服务端(SSR)+ 客户端
能否使用 Hooks不能(useState, useEffect 等)可以
能否使用浏览器 API不能(window, document 等)可以
能否直接访问数据库可以不可以
能否使用 async/await可以(组件级别)不可以(需要 useEffect)
JS Bundle 体积零(不发送到客户端)包含在 JS Bundle 中
适用场景数据获取、静态内容交互、状态、事件处理

使用示例

app/posts/page.tsx(Server Component - 默认)
// 这是一个 Server Component,可以直接 async/await
import { db } from '@/lib/db';
import LikeButton from './like-button';

export default async function PostsPage() {
const posts = await db.post.findMany(); // 直接访问数据库

return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
<LikeButton postId={post.id} /> {/* Client Component */}
</article>
))}
</div>
);
}
app/posts/like-button.tsx(Client Component)
'use client';

import { useState } from 'react';

interface LikeButtonProps {
postId: string;
}

export default function LikeButton({ postId }: LikeButtonProps) {
const [liked, setLiked] = useState(false);

return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'} 点赞
</button>
);
}

'use client' 边界

注意

'use client' 声明的是一个边界,而不仅仅标记一个组件。该文件及其导入的所有子模块都会成为 Client Components。因此应该尽量将 'use client' 边界下推到组件树的叶子节点。

组合模式

Server Component 可以将其他 Server Component 作为 children 传递给 Client Component,这样即使包裹在 Client Component 中,Server Component 也不会变成 Client Component:

// ClientWrapper 是 Client Component
// ServerContent 仍然是 Server Component
<ClientWrapper>
<ServerContent />
</ClientWrapper>

5. 数据获取

App Router 数据获取方式

App Router 中的数据获取方式完全不同于 Pages Router,主要通过以下方式:

1. Server Components 中直接 fetch

app/users/page.tsx
interface User {
id: number;
name: string;
email: string;
}

export default async function UsersPage() {
const res = await fetch('https://api.example.com/users', {
next: {
revalidate: 3600, // ISR: 1 小时后 revalidate
tags: ['users'], // 缓存标签,可按需 revalidate
},
});

if (!res.ok) throw new Error('Failed to fetch users');

const users: User[] = await res.json();

return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
);
}

2. generateStaticParams(替代 getStaticPaths)

app/blog/[slug]/page.tsx
interface Post {
slug: string;
title: string;
content: string;
}

// 构建时生成静态路径
export async function generateStaticParams() {
const posts: Post[] = await fetch('https://api.example.com/posts').then(
(res) => res.json()
);

return posts.map((post) => ({
slug: post.slug,
}));
}

export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post: Post = await fetch(
`https://api.example.com/posts/${slug}`
).then((res) => res.json());

return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}

3. Server Actions('use server')

Server Actions 允许在客户端直接调用服务端函数,无需手动创建 API 端点。这是 React 19 Actions 在 Next.js 中的最佳实践:

app/actions/post.ts
'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

interface CreatePostData {
title: string;
content: string;
}

export async function createPost(data: CreatePostData) {
// 直接在服务端操作数据库
const post = await db.post.create({
data: {
title: data.title,
content: data.content,
},
});

revalidatePath('/posts'); // 清除缓存,触发重新渲染
return post;
}

export async function deletePost(postId: string) {
await db.post.delete({ where: { id: postId } });
revalidatePath('/posts');
}
app/posts/create-form.tsx
'use client';

import { useActionState } from 'react';
import { createPost } from '@/app/actions/post';

export default function CreatePostForm() {
const [state, formAction, isPending] = useActionState(
async (_prevState: unknown, formData: FormData) => {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
return await createPost({ title, content });
},
null
);

return (
<form action={formAction}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" required />
<button type="submit" disabled={isPending}>
{isPending ? '发布中...' : '发布'}
</button>
</form>
);
}
关键点

Server Actions 的优势在于:

  1. 类型安全:函数签名在客户端和服务端共享
  2. 渐进增强:即使 JavaScript 未加载,表单也能提交
  3. 自动封装:Next.js 自动将 Server Actions 封装为 POST 请求

6. 路由系统

文件系统路由

App Router 的路由完全基于 app/ 目录下的文件结构:

路由模式目录结构URL 示例
静态路由app/about/page.tsx/about
动态路由app/blog/[slug]/page.tsx/blog/hello-world
Catch-allapp/docs/[...slug]/page.tsx/docs/a/b/c
Optional Catch-allapp/docs/[[...slug]]/page.tsx/docs/docs/a/b
路由组app/(shop)/products/page.tsx/products
并行路由app/@modal/login/page.tsx同一 URL 渲染多个页面
拦截路由app/feed/(..)photo/[id]/page.tsx拦截 /photo/[id]

动态路由

app/products/[category]/[id]/page.tsx
interface ProductPageProps {
params: Promise<{
category: string;
id: string;
}>;
}

export default async function ProductPage({ params }: ProductPageProps) {
const { category, id } = await params;

const product = await fetch(
`https://api.example.com/products/${category}/${id}`
).then((res) => res.json());

return <ProductDetail product={product} />;
}

路由组 (Route Groups)

路由组使用 (folderName) 语法,允许在不影响 URL 结构的情况下组织路由:

app/
├── (marketing)/ # 路由组:营销页面
│ ├── layout.tsx # 营销页面专用布局
│ ├── page.tsx # / (首页)
│ ├── about/
│ │ └── page.tsx # /about
│ └── pricing/
│ └── page.tsx # /pricing
├── (dashboard)/ # 路由组:管理后台
│ ├── layout.tsx # 后台专用布局(侧边栏等)
│ ├── dashboard/
│ │ └── page.tsx # /dashboard
│ └── settings/
│ └── page.tsx # /settings

并行路由 (Parallel Routes)

并行路由使用 @folder 约定,允许在同一布局中同时渲染多个页面:

app/layout.tsx
export default function Layout({
children,
modal,
analytics,
}: {
children: React.ReactNode;
modal: React.ReactNode; // 对应 app/@modal/
analytics: React.ReactNode; // 对应 app/@analytics/
}) {
return (
<div>
{children}
{modal}
{analytics}
</div>
);
}

拦截路由 (Intercepting Routes)

拦截路由用于在当前布局中加载另一个路由的内容(如在 Feed 列表中弹窗展示图片详情):

约定匹配
(.)同级路由
(..)上一级路由
(..)(..)上两级路由
(...)根路由
app/
├── feed/
│ ├── page.tsx # /feed
│ └── (..)photo/[id]/ # 拦截 /photo/[id]
│ └── page.tsx # 在 feed 布局中展示弹窗
├── photo/[id]/
│ └── page.tsx # /photo/[id] 完整页面

Middleware

Middleware 在请求到达路由之前执行,可用于认证、重定向、国际化等:

middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// 认证检查
const token = request.cookies.get('auth-token');

if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}

// 国际化:根据 Accept-Language 重定向
const locale = request.headers.get('accept-language')?.split(',')[0];
if (locale?.startsWith('zh') && !request.nextUrl.pathname.startsWith('/zh')) {
return NextResponse.redirect(
new URL(`/zh${request.nextUrl.pathname}`, request.url)
);
}

return NextResponse.next();
}

// 配置 Middleware 匹配的路由
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};

7. 性能优化

Next.js 内置了多种性能优化方案,也可结合 首屏优化 中的通用策略。

next/image 图片优化

app/components/hero.tsx
import Image from 'next/image';

export default function Hero() {
return (
<div>
<Image
src="/hero.jpg"
alt="Hero Image"
width={1200}
height={600}
priority // LCP 图片,预加载
placeholder="blur" // 模糊占位符
blurDataURL="data:image/..." // 自定义模糊占位
/>

{/* 响应式图片 */}
<Image
src="/photo.jpg"
alt="Photo"
fill // 填充父容器
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover' }}
/>
</div>
);
}
next/image 自动优化能力
  • 自动转换为 WebP/AVIF 格式
  • 按需生成不同尺寸
  • 懒加载(默认开启)
  • 防止布局偏移(CLS)
  • 支持远程图片优化

next/font 字体优化

app/layout.tsx
import { Inter, Noto_Sans_SC } from 'next/font/google';

const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});

const notoSansSC = Noto_Sans_SC({
subsets: ['latin'],
weight: ['400', '700'],
display: 'swap',
variable: '--font-noto',
});

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN" className={`${inter.variable} ${notoSansSC.variable}`}>
<body>{children}</body>
</html>
);
}
关键点

next/font 在构建时自动下载字体文件并自托管,消除了对 Google Fonts CDN 的运行时请求,实现零布局偏移(FOUT/FOIT)。

Streaming 与 Suspense

Streaming 允许服务端逐步发送 HTML,配合 React 18 的 Suspense 实现渐进式渲染:

app/dashboard/page.tsx
import { Suspense } from 'react';

// 慢数据组件 - 独立 Streaming
async function SlowAnalytics() {
const data = await fetch('https://api.example.com/analytics', {
cache: 'no-store',
}).then((res) => res.json());
return <AnalyticsChart data={data} />;
}

// 快数据组件
async function QuickStats() {
const stats = await fetch('https://api.example.com/stats').then(
(res) => res.json()
);
return <StatsCard stats={stats} />;
}

export default function Dashboard() {
return (
<div>
<h1>仪表盘</h1>
{/* 快数据先展示 */}
<Suspense fallback={<div>加载统计数据...</div>}>
<QuickStats />
</Suspense>
{/* 慢数据后展示,不阻塞页面 */}
<Suspense fallback={<div>加载分析图表...</div>}>
<SlowAnalytics />
</Suspense>
</div>
);
}

Partial Prerendering (PPR)

PPR 是 Next.js 14 引入的实验性特性,结合了 Static 和 Dynamic 渲染的优势:

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
experimental: {
ppr: true, // 启用 Partial Prerendering
},
};

export default nextConfig;
app/product/[id]/page.tsx
import { Suspense } from 'react';

// 静态部分:构建时预渲染
function ProductInfo({ id }: { id: string }) {
return <div>商品 #{id} 的静态信息</div>;
}

// 动态部分:用 Suspense 包裹
async function PersonalizedRecommendations() {
const recs = await fetch('https://api.example.com/recommendations', {
cache: 'no-store', // 动态数据
}).then((res) => res.json());
return <RecommendationList items={recs} />;
}

export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<div>
{/* 这部分是 Static Shell,从 CDN 提供 */}
<ProductInfo id={id} />
{/* 这部分在请求时 Streaming 渲染 */}
<Suspense fallback={<div>加载推荐...</div>}>
<PersonalizedRecommendations />
</Suspense>
</div>
);
}
PPR 的本质

PPR = 静态 Shell(构建时生成,CDN 缓存) + 动态 Holes(请求时 Streaming 填充)。用户立即看到静态部分,动态部分逐步加载,兼顾了 SSG 的速度和 SSR 的数据时效性。


8. 缓存机制

Next.js 拥有一套多层缓存机制,理解它对于性能调优和解决数据不更新问题至关重要。

四层缓存架构

四层缓存详解

缓存层位置持久性作用失效方式
Request Memoization服务端单次请求同一请求中相同 fetch 自动去重请求结束自动清除
Data Cache服务端持久化(跨请求、跨部署)缓存 fetch 返回的数据revalidaterevalidateTagrevalidatePath
Full Route Cache服务端持久化(跨请求)缓存静态路由的 HTML 和 RSC PayloadrevalidatePathrevalidateTagdynamic API
Router Cache客户端会话级(内存)缓存已访问路由的 RSC Payloadrouter.refresh()revalidatePath、Cookie 变化

缓存控制

app/lib/data.ts
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';

// 1. fetch 级缓存控制
async function getPostsWithFetch() {
// 不缓存(每次请求都获取最新数据)
const res = await fetch('https://api.example.com/posts', { cache: 'no-store' });
return res.json();
}

// 2. 基于时间的 revalidation
async function getPostsWithRevalidate() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }, // 60 秒后重新验证
});
return res.json();
}

// 3. 基于标签的 revalidation
async function getPostsByTag() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }, // 标记缓存标签
});
return res.json();
}

// 4. 非 fetch 场景的缓存(ORM、数据库直连)
const getCachedPosts = unstable_cache(
async () => {
return db.post.findMany();
},
['posts'], // 缓存 key
{
revalidate: 3600, // 1 小时
tags: ['posts'], // 缓存标签
}
);

主动清除缓存

app/actions/revalidate.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

// 按路径清除
export async function revalidatePostsPage() {
revalidatePath('/posts'); // 清除特定路径缓存
revalidatePath('/posts', 'layout'); // 清除布局缓存
revalidatePath('/', 'layout'); // 清除所有缓存
}

// 按标签清除(推荐,更精确)
export async function revalidatePostsData() {
revalidateTag('posts'); // 清除所有带 'posts' 标签的缓存
}
缓存陷阱

Next.js 的缓存默认是积极缓存的(默认 cache: 'force-cache'),这意味着如果不主动配置 revalidatecache: 'no-store',数据可能一直返回旧值。这是新手最常遇到的问题之一。

Next.js 15 中,fetch 的默认缓存行为改为了 cache: 'no-store',即默认不缓存。


9. 部署

Vercel 部署

最简单的方式,推送代码到 GitHub 后自动部署:

# 安装 Vercel CLI
npm i -g vercel

# 一键部署
vercel

standalone 输出 + Docker

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
output: 'standalone', // 生成独立运行的最小化输出
};

export default nextConfig;
Dockerfile
FROM node:20-alpine AS base

# 依赖安装
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile

# 构建
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable pnpm && pnpm build

# 生产镜像
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# 只复制 standalone 输出
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

USER nextjs
EXPOSE 3000
ENV PORT=3000

CMD ["node", "server.js"]

静态导出

对于纯静态站点,可以导出为纯 HTML/CSS/JS:

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
output: 'export', // 静态导出
};

export default nextConfig;
注意

静态导出不支持以下特性:

  • Server Components(动态数据获取)
  • Server Actions
  • Middleware
  • ISR / revalidate
  • next/image 的默认图片优化(需要自定义 loader)

适用于博客、文档站等纯静态内容。


常见面试问题

Q1: Next.js 的渲染模式有哪些?分别适用什么场景?

答案

Next.js 支持四种主要渲染模式:

模式说明适用场景App Router 实现
CSR客户端渲染,浏览器执行 JS 渲染后台管理系统、不需要 SEO'use client' 组件
SSR每次请求在服务端渲染 HTML个性化内容、实时数据cache: 'no-store'
SSG构建时生成静态 HTML博客、文档、营销页默认行为 + generateStaticParams
ISR构建时生成 + 按需增量更新电商商品页、新闻列表next: { revalidate: N }

选择策略

代码示例

各模式的 App Router 实现
// SSG(默认)
export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{/* ... */}</div>;
}

// SSR
export default async function Page() {
const data = await fetch('https://api.example.com/data', { cache: 'no-store' });
return <div>{/* ... */}</div>;
}

// ISR
export default async function Page() {
const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 } });
return <div>{/* ... */}</div>;
}

// 也可以使用路由段配置
export const dynamic = 'force-dynamic'; // SSR
export const revalidate = 60; // ISR
export const dynamic = 'force-static'; // SSG

Q2: App Router 和 Pages Router 有什么区别?为什么推荐 App Router?

答案

App Router 是 Next.js 13+ 引入的新路由架构,相比 Pages Router 有以下核心改进:

1. Server Components 默认启用

// Pages Router: 所有组件都是 Client Components
// 数据获取需要通过特定函数
export async function getServerSideProps() {
const data = await fetch('...');
return { props: { data: await data.json() } };
}

export default function Page({ data }: { data: any }) {
return <div>{/* 使用 data */}</div>;
}
// App Router: 默认 Server Components,直接 async/await
export default async function Page() {
const data = await fetch('...');
const json = await data.json();
return <div>{/* 直接使用 json */}</div>;
}

2. 嵌套布局取代全局布局

// Pages Router: 只能通过 _app.tsx 设置全局布局
// App Router: 每个路由层级都可以有独立 layout.tsx,且跨导航保持状态

3. 粒度化的 Loading 和 Error 处理

Pages Router 只有全局的 _error.tsx,而 App Router 每个路由段都可以有独立的 loading.tsxerror.tsx,结合 Suspense 实现更细粒度的加载状态。

4. 推荐 App Router 的原因

  • 默认 Server Components,减小 JS Bundle 体积
  • Streaming + Suspense 提升首屏体验
  • 嵌套布局更灵活,导航更高效
  • Server Actions 简化数据变更
  • 与 React 19 特性深度集成(参考 React 19 新特性
  • Pages Router 已进入维护模式,新特性只在 App Router 上开发

Q3: Server Components 和 Client Components 的区别是什么?

答案

这是 Next.js 面试中最高频的问题之一。Server Components 是 React 19 的核心特性(参考 React 19 新特性),Next.js App Router 默认使用 Server Components。

核心区别

特性Server ComponentsClient Components
标记方式默认(无需标记)'use client'
渲染位置仅服务端服务端预渲染 + 客户端 Hydration
JS Bundle不包含(零 JS)包含
useState/useEffect不可用可用
onClick 等事件不可用可用
async 组件支持不支持
直接访问后端支持(数据库、文件系统)不支持
导入 Server Component可以仅通过 children 传入

选择原则

// 需要用 Server Component 的场景:
// - 数据获取
// - 访问后端资源
// - 敏感信息(API key、数据库连接)
// - 大型依赖(不需要发送到客户端)

// 需要用 Client Component 的场景:
// - 交互(onClick, onChange 等)
// - 状态管理(useState, useReducer)
// - 生命周期/副作用(useEffect)
// - 浏览器 API(window, localStorage)
// - 自定义 Hooks(依赖 state/effect 的)

组合模式最佳实践

app/posts/page.tsx(Server Component)
import { db } from '@/lib/db';
import SearchBar from './search-bar'; // Client Component

export default async function PostsPage() {
const posts = await db.post.findMany(); // 服务端直接查数据库

return (
<div>
{/* Client Component: 处理搜索交互 */}
<SearchBar />
{/* Server Component: 静态渲染列表,零 JS */}
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.summary}</p>
</article>
))}
</div>
);
}

Q4: Next.js 的缓存机制是怎样的?

答案

Next.js 有四层缓存,从客户端到服务端依次为:

1. Router Cache(客户端内存缓存)

  • 缓存已访问路由的 RSC Payload
  • 用户后退/前进时即时加载
  • 通过 router.refresh() 失效

2. Full Route Cache(服务端)

  • 缓存静态路由的完整 HTML 和 RSC Payload
  • 仅对静态路由生效,动态路由不缓存
  • 通过 revalidatePath / revalidateTag 失效

3. Data Cache(服务端持久化)

  • 缓存 fetch 请求的返回数据
  • 跨请求、跨部署持久化(Vercel 等平台)
  • 通过 revalidate 时间或 revalidateTag 失效

4. Request Memoization(单次请求去重)

  • 同一次服务端渲染中,相同 URL 的 fetch 自动去重
  • 无需手动管理,React 自动处理
缓存控制示例
// 不缓存
const data = await fetch(url, { cache: 'no-store' });

// 缓存 60 秒
const data = await fetch(url, { next: { revalidate: 60 } });

// 打标签缓存
const data = await fetch(url, { next: { tags: ['products'] } });

// 清除标签缓存
import { revalidateTag } from 'next/cache';
revalidateTag('products');

// 清除路径缓存
import { revalidatePath } from 'next/cache';
revalidatePath('/products');
面试高频坑点
  • Next.js 14 默认 fetch 使用 force-cache(积极缓存),Next.js 15 改为 no-store(不缓存)
  • revalidatePath 会同时清除 Data Cache 和 Full Route Cache
  • Router Cache 在客户端,服务端的 revalidate 不能直接清除它
  • fetch 的数据获取(如 ORM)不会自动缓存,需要使用 unstable_cache

Q5: 如何在 Next.js 中实现国际化?

答案

Next.js App Router 中实现国际化的推荐方式是通过 子路径路由 + Middleware

1. 目录结构

app/
├── [locale]/ # 动态语言段
│ ├── layout.tsx
│ ├── page.tsx # /zh、/en
│ ├── about/
│ │ └── page.tsx # /zh/about、/en/about
│ └── dictionaries.ts # 字典加载
├── middleware.ts # 语言检测和重定向

2. Middleware 语言检测

middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';

const locales = ['en', 'zh', 'ja'];
const defaultLocale = 'en';

function getLocale(request: NextRequest): string {
const headers: Record<string, string> = {};
request.headers.forEach((value, key) => {
headers[key] = value;
});
const languages = new Negotiator({ headers }).languages();
return match(languages, locales, defaultLocale);
}

export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;

// 检查路径是否已包含语言前缀
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);

if (pathnameHasLocale) return;

// 重定向到正确的语言路径
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}

export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

3. 字典文件和加载

app/[locale]/dictionaries.ts
const dictionaries: Record<string, () => Promise<Record<string, string>>> = {
en: () => import('./dictionaries/en.json').then((m) => m.default),
zh: () => import('./dictionaries/zh.json').then((m) => m.default),
ja: () => import('./dictionaries/ja.json').then((m) => m.default),
};

export const getDictionary = async (locale: string) => {
const loader = dictionaries[locale] ?? dictionaries['en'];
return loader();
};
app/[locale]/page.tsx
import { getDictionary } from './dictionaries';

export default async function Home({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const dict = await getDictionary(locale);

return (
<div>
<h1>{dict.title}</h1>
<p>{dict.description}</p>
</div>
);
}
更完整的国际化方案(next-intl 库)

对于复杂的国际化需求(复数、日期格式化、嵌套翻译等),推荐使用 next-intl 库:

npm install next-intl
i18n/request.ts
import { getRequestConfig } from 'next-intl/server';

export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`../messages/${locale}.json`)).default,
}));

next-intl 提供了类型安全的翻译函数、ICU 消息格式支持和与 App Router 的深度集成。


Q6: Next.js 的 Middleware 是什么?有哪些应用场景?

答案

Next.js Middleware 运行在 Edge Runtime,在请求到达页面之前执行,可以对请求进行拦截、重写和重定向。

middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;

// 场景1:认证检查
const token = request.cookies.get('token')?.value;
if (pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}

// 场景2:A/B 测试
const bucket = request.cookies.get('ab-bucket')?.value || (Math.random() > 0.5 ? 'A' : 'B');
const response = NextResponse.next();
response.cookies.set('ab-bucket', bucket);
if (bucket === 'B') {
return NextResponse.rewrite(new URL('/experiment' + pathname, request.url));
}

return response;
}

export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

核心应用场景

场景实现方式
认证鉴权检查 Cookie/Token,未登录重定向到登录页
国际化路由检测 Accept-Language,重定向到对应语言路径
A/B 测试按用户分桶,rewrite 到不同页面变体
Bot 检测检查 User-Agent,爬虫返回 SSR 版本
地域限制根据 request.geo 限制特定地区访问
请求头注入为下游 Server Components 添加自定义 Header
限制

Middleware 运行在 Edge Runtime,不能使用 Node.js API(如 fspath)。也不能直接访问数据库,只能做轻量的请求级处理。


Q7: next/image 做了哪些优化?为什么推荐使用?

答案

next/image 相比原生 <img> 标签提供了五大优化:

优化项说明
格式转换自动将 JPG/PNG 转为 WebP/AVIF(体积减少 30-50%)
尺寸适配根据设备 DPR 和容器尺寸生成多规格图片(srcset
懒加载默认 loading="lazy",只有进入视口才加载
占位防抖支持 placeholder="blur" 显示模糊占位图,防止 CLS
CDN 缓存生成的图片缓存在 /_next/image,带 Cache-Control
使用示例
import Image from 'next/image';

// 远程图片(需在 next.config.js 配置 remotePatterns)
<Image
src="https://cdn.example.com/photo.jpg"
alt="Photo"
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQ..." // 小尺寸 base64
quality={80}
priority={false} // 首屏关键图片设为 true,跳过懒加载
/>

// fill 模式(父容器需设 position: relative)
<div style={{ position: 'relative', width: '100%', aspectRatio: '16/9' }}>
<Image src="/hero.jpg" alt="Hero" fill sizes="100vw" style={{ objectFit: 'cover' }} />
</div>
priority 属性

对于首屏关键图片(如 Hero Banner),务必设置 priority={true},这会禁用懒加载并添加 <link rel="preload">,直接改善 LCP 指标。


Q8: Server Actions 是什么?和传统 API Routes 有什么区别?

答案

Server Actions 是 Next.js 14+ 引入的服务端数据变更方案,允许在组件中直接定义服务端函数,替代传统的 API Routes。

Server Action 示例
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

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'); // 自动刷新缓存
}
在 Server Component 中使用(表单)
import { createPost } from './actions';

export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">发布</button>
</form>
);
}

与 API Routes 的对比

维度Server ActionsAPI Routes
调用方式直接调用函数 / <form action>fetch('/api/...')
类型安全函数签名直接提供类型需要手动定义 Request/Response 类型
缓存集成revalidatePath/revalidateTag 直接调用需要额外调用
渐进增强JS 禁用时表单仍可工作JS 禁用时完全不可用
适用场景数据变更(CRUD)第三方 Webhook、非 Next.js 客户端调用

Q9: Next.js 的 Streaming 和 Suspense 是怎么配合的?

答案

传统 SSR 需要等待所有数据获取完成才能发送 HTML,Streaming 允许分块发送 HTML,配合 React Suspense 实现渐进式渲染。

Streaming 实现
import { Suspense } from 'react';

export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
{/* 立即渲染 */}
<ProductInfo id={params.id} />

{/* 异步 Streaming */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews id={params.id} /> {/* async Server Component */}
</Suspense>

<Suspense fallback={<RecommendSkeleton />}>
<Recommendations id={params.id} />
</Suspense>
</div>
);
}

优势:TTFB 大幅降低(不等最慢的数据),用户更快看到页面内容,FCP/LCP 指标改善。


Q10: 什么是 Partial Prerendering(PPR)?

答案

PPR 是 Next.js 15 引入的实验性特性,将静态外壳动态内容结合在同一个路由中:

  • 页面的静态部分在构建时预渲染为 HTML(像 SSG)
  • 动态部分用 Suspense 包裹,在请求时 Streaming 填充(像 SSR)
PPR 示例
// 静态外壳 + 动态孔洞
export default function ProductPage() {
return (
<div>
<Header /> {/* 静态:构建时预渲染 */}
<ProductInfo /> {/* 静态:构建时预渲染 */}

{/* 动态孔洞:请求时 Streaming */}
<Suspense fallback={<CartSkeleton />}>
<Cart /> {/* 动态:依赖用户登录状态 */}
</Suspense>

<Footer /> {/* 静态:构建时预渲染 */}
</div>
);
}

PPR 的核心价值是不再需要在 SSG 和 SSR 之间二选一——同一个页面可以同时拥有静态的高性能和动态的个性化。


Q11: 'use client''use server' 指令分别什么时候用?

答案

指令含义使用时机
'use client'标记组件为 Client Component需要 useState/useEffect/事件处理/浏览器 API 时
'use server'标记函数为 Server Action需要在服务端执行数据变更(写入数据库、调用外部 API)时

关键理解'use client' 不代表组件只在客户端渲染——它仍然会在服务端预渲染 HTML,但它的 JS 会包含在 Bundle 中并在客户端 Hydration。

// ❌ 常见误区:以为 'use client' = 纯客户端渲染
// 实际上:Server 预渲染 HTML → 发送到浏览器 → Hydration 激活交互

// ✅ 正确理解
// 'use client' 是一个"边界声明",它以下的所有 import 都进入客户端 Bundle
// 所以应该尽量将 'use client' 放在组件树的叶子节点
最佳实践

'use client' 推到组件树的最底层。不要在 layout.tsx 或页面组件上标记 'use client',而是只在真正需要交互的小组件上标记。


Q12: Next.js 中如何做 SEO 优化?

答案

Next.js App Router 提供了内置的 Metadata API:

app/products/[id]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';

// 动态生成 Metadata
export async function generateMetadata(
{ params }: { params: Promise<{ id: string }> },
parent: ResolvingMetadata,
): Promise<Metadata> {
const { id } = await params;
const product = await fetch(`https://api.example.com/products/${id}`).then(r => r.json());

return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [product.image],
},
alternates: {
canonical: `https://example.com/products/${id}`,
languages: { 'zh-CN': `/zh/products/${id}`, 'en-US': `/en/products/${id}` },
},
};
}

SEO 优化清单

优化项Next.js 实现方式
标题和描述generateMetadata / metadata 导出
Open Graphmetadata.openGraph
结构化数据页面中输出 <script type="application/ld+json">
Sitemapapp/sitemap.ts 导出 sitemap() 函数
Robotsapp/robots.ts 导出 robots() 函数
Canonical URLmetadata.alternates.canonical
多语言 hreflangmetadata.alternates.languages

Q13: generateStaticParams 的作用是什么?

答案

generateStaticParams 用于在构建时为动态路由生成静态页面(类似 Pages Router 的 getStaticPaths)。

app/blog/[slug]/page.tsx
// 构建时生成所有博客文章的静态页面
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post: { slug: string }) => ({ slug: post.slug }));
}

export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json());
return <article>{post.content}</article>;
}
与 getStaticPaths 的区别getStaticPaths (Pages)generateStaticParams (App)
返回格式{ paths: [{ params }], fallback }直接返回 params 数组
fallback 控制fallback: true/false/blocking通过 dynamicParams 配置
多层嵌套每层独立定义支持自动继承父级 params

Q14: Next.js 的 loading.tsxerror.tsx 是怎么工作的?

答案

App Router 通过文件约定实现路由级别的 Loading 和 Error UI,底层分别封装了 React 的 Suspense 和 Error Boundary:

app/
├── layout.tsx → 包裹所有子路由
├── loading.tsx → 自动包装为 <Suspense fallback={<Loading />}>
├── error.tsx → 自动包装为 <ErrorBoundary fallback={<Error />}>
├── not-found.tsx → notFound() 触发时显示
└── page.tsx → 页面内容
app/dashboard/loading.tsx
// 路由切换时自动显示
export default function Loading() {
return <div className="skeleton">加载中...</div>;
}
app/dashboard/error.tsx
'use client'; // Error Boundary 必须是 Client Component

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

每个路由段的 error.tsx 只捕获该段及子段的错误,不影响父级布局。loading.tsx 同理——只在当前段的内容加载时显示,不影响已渲染的外层布局。


Q15: 如何用 Docker 部署 Next.js?standalone 输出模式是什么?

答案

output: 'standalone' 会让 Next.js 生成一个自包含的 server.js,只包含运行所需的最小文件集,无需 node_modules

next.config.ts
const config = {
output: 'standalone', // 生成独立部署包
};
export default config;
Dockerfile(多阶段构建)
# 阶段 1:安装依赖
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile

# 阶段 2:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable && pnpm build

# 阶段 3:运行(最小镜像)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# 只复制 standalone 输出 + 静态文件
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]
部署方式镜像体积冷启动适用场景
带 node_modules~1GB+不推荐
standalone~100-200MBDocker/K8s 部署(推荐)
静态导出 (export)~几十MB纯静态站点

相关链接