跳到主要内容

Next.js 核心知识

问题​

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

面试速答版

什么是 Next.js? Vercel 出的 React 全栈框架,把 SSR、文件路由、代码分割、图片优化、API 等能力打包成「零配置开箱即用」,目前是 React 官方推荐的生产级框架。

它解决了哪些 React 开发中的痛点?

  • 渲染模式单一:纯 React 只能 CSR,首屏白、SEO 差;Next.js 支持 CSR / SSR / SSG / ISR / Streaming,可以同一个项目混用。
  • 路由配置繁琐:文件即路由,app/blog/[slug]/page.tsx 直接对应 /blog/:slug,嵌套布局靠 layout.tsx。
  • 数据获取 + Loading/Error 模板代码:Server Component 里直接 async/await,约定 loading.tsx、error.tsx、not-found.tsx。
  • 后端接口要单独搭:route.ts 写 API,Server Actions 直接在组件里调函数变更数据。
  • 图片/字体/打包优化:next/image、next/font 自动处理,按路由自动代码分割。

App Router、Server Components、缓存机制等核心概念如何理解?

  • App Router:基于 RSC + 嵌套布局的新路由(app/ 目录),layout.tsx 在导航时不重渲染、状态保留;(group) 是路由组不影响 URL;推荐新项目用,Pages Router 进入维护模式。
  • Server Components:app/ 下默认就是 SC,能 async/await、能直接查库,代码不打到客户端 bundle;交互/Hooks 加 'use client' 切换。
  • 缓存四层:Request Memoization(同一请求内 fetch 去重)、Data Cache(fetch 跨请求持久缓存,按 revalidate 失效)、Full Route Cache(构建期 HTML 缓存)、Router Cache(浏览器内已访问路由的 RSC payload 缓存)。控制方式:cache: 'no-store' 关 Data Cache、next: { revalidate: 60 } 设 ISR、revalidatePath/revalidateTag 主动失效。

答案​

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 渲染,无需手动声明 getServerSideProps 或 getStaticProps。


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 返回的数据revalidate、revalidateTag、revalidatePath
Full Route Cache服务端持久化(跨请求)缓存静态路由的 HTML 和 RSC PayloadrevalidatePath、revalidateTag、dynamic 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'),这意味着如果不主动配置 revalidate 或 cache: '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.tsx 和 error.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(如 fs、path)。也不能直接访问数据库,只能做轻量的请求级处理。


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.tsx 和 error.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-200MB快Docker/K8s 部署(推荐)
静态导出 (export)~几十MB无纯静态站点

相关链接​