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 |
|---|---|---|
| 渲染模式 | 仅 CSR | CSR / 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 12 | Middleware、SWC 编译器、ISR 改进 |
| Next.js 13 | App Router(beta)、Server Components、Turbopack |
| Next.js 14 | App Router 稳定、Server Actions 稳定、Partial Prerendering(preview) |
| Next.js 15 | React 19 支持、Turbopack Dev 稳定、异步请求 API |
2. 渲染模式
Next.js 支持多种渲染模式,每种模式适用于不同的场景。关于 SSR/SSG 的更多细节可参考 SSR 与 SSG 和 首屏优化。
渲染流程对比
模式对比表
| 特性 | CSR | SSR | SSG | ISR |
|---|---|---|---|---|
| 渲染时机 | 运行时(浏览器) | 运行时(服务器) | 构建时 | 构建时 + 按需更新 |
| 首屏速度 | 慢 | 快 | 最快 | 快 |
| SEO | 差 | 好 | 好 | 好 |
| 数据时效性 | 实时 | 实时 | 构建时 | 可配置(秒级) |
| 服务器压力 | 无 | 高 | 无 | 低 |
| 适用场景 | 后台管理系统 | 个性化内容 | 博客/文档 | 电商/新闻 |
App Router 中的实现
在 App Router 中,渲染模式通过组件类型和 fetch 配置来控制:
// 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 Router | App Router |
|---|---|---|
| 目录 | pages/ | app/ |
| 路由文件 | 任意文件名即路由 | 必须使用 page.tsx |
| 布局 | _app.tsx + _document.tsx(全局) | layout.tsx(嵌套布局) |
| 数据获取 | getServerSideProps / getStaticProps | async Server Components / fetch |
| Server Components | 不支持 | 默认支持 |
| Streaming | 不支持 | 原生支持 |
| 加载状态 | 手动实现 | loading.tsx 约定 |
| 错误处理 | _error.tsx(全局) | error.tsx(嵌套) |
| Middleware | 支持 | 支持 |
| API | pages/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:
// 根布局:每个页面都会使用
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>
<nav>全局导航栏</nav>
{children}
<footer>全局页脚</footer>
</body>
</html>
);
}
// 仪表盘布局:仅 /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
// 自动包裹在 Suspense 中,作为 fallback
export default function Loading() {
return <div className="skeleton">加载中...</div>;
}
'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 Components | Client Components |
|---|---|---|
| 默认行为 | App Router 中默认 | 需要 'use client' 声明 |
| 执行环境 | 仅服务端 | 服务端(SSR)+ 客户端 |
| 能否使用 Hooks | 不能(useState, useEffect 等) | 可以 |
| 能否使用浏览器 API | 不能(window, document 等) | 可以 |
| 能否直接访问数据库 | 可以 | 不可以 |
| 能否使用 async/await | 可以(组件级别) | 不可以(需要 useEffect) |
| JS Bundle 体积 | 零(不发送到客户端) | 包含在 JS Bundle 中 |
| 适用场景 | 数据获取、静态内容 | 交互、状态、事件处理 |
使用示例
// 这是一个 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>
);
}
'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
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)
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 中的最佳实践:
'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');
}
'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 的优势在于:
- 类型安全:函数签名在客户端和服务端共享
- 渐进增强:即使 JavaScript 未加载,表单也能提交
- 自动封装:Next.js 自动将 Server Actions 封装为 POST 请求
6. 路由系统
文件系统路由
App Router 的路由完全基于 app/ 目录下的文件结构:
| 路由模式 | 目录结构 | URL 示例 |
|---|---|---|
| 静态路由 | app/about/page.tsx | /about |
| 动态路由 | app/blog/[slug]/page.tsx | /blog/hello-world |
| Catch-all | app/docs/[...slug]/page.tsx | /docs/a/b/c |
| Optional Catch-all | app/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] |
动态路由
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 约定,允许在同一布局中同时渲染多个页面:
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 在请求到达路由之前执行,可用于认证、重定向、国际化等:
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 图片优化
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>
);
}
- 自动转换为 WebP/AVIF 格式
- 按需生成不同尺寸
- 懒加载(默认开启)
- 防止布局偏移(CLS)
- 支持远程图片优化
next/font 字体优化
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 实现渐进式渲染:
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 渲染的优势:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
ppr: true, // 启用 Partial Prerendering
},
};
export default nextConfig;
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 = 静态 Shell(构建时生成,CDN 缓存) + 动态 Holes(请求时 Streaming 填充)。用户立即看到静态部分,动态部分逐步加载,兼顾了 SSG 的速度和 SSR 的数据时效性。
8. 缓存机制
Next.js 拥有一套多层缓存机制,理解它对于性能调优和解决数据不更新问题至关重要。
四层缓存架构
四层缓存详解
| 缓存层 | 位置 | 持久性 | 作用 | 失效方式 |
|---|---|---|---|---|
| Request Memoization | 服务端 | 单次请求 | 同一请求中相同 fetch 自动去重 | 请求结束自动清除 |
| Data Cache | 服务端 | 持久化(跨请求、跨部署) | 缓存 fetch 返回的数据 | revalidate、revalidateTag、revalidatePath |
| Full Route Cache | 服务端 | 持久化(跨请求) | 缓存静态路由的 HTML 和 RSC Payload | revalidatePath、revalidateTag、dynamic API |
| Router Cache | 客户端 | 会话级(内存) | 缓存已访问路由的 RSC Payload | router.refresh()、revalidatePath、Cookie 变化 |
缓存控制
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'], // 缓存标签
}
);
主动清除缓存
'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
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'standalone', // 生成独立运行的最小化输出
};
export default nextConfig;
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:
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 } |
选择策略:
代码示例:
// 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 Components | Client 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 的)
组合模式最佳实践:
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 语言检测
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. 字典文件和加载
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();
};
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
- Yarn
- pnpm
- Bun
npm install next-intl
yarn add next-intl
pnpm add next-intl
bun add next-intl
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,在请求到达页面之前执行,可以对请求进行拦截、重写和重定向。
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>
对于首屏关键图片(如 Hero Banner),务必设置 priority={true},这会禁用懒加载并添加 <link rel="preload">,直接改善 LCP 指标。
Q8: Server Actions 是什么?和传统 API Routes 有什么区别?
答案:
Server Actions 是 Next.js 14+ 引入的服务端数据变更方案,允许在组件中直接定义服务端函数,替代传统的 API Routes。
// 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'); // 自动刷新缓存
}
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 Actions | API 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 实现渐进式渲染。
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)
// 静态外壳 + 动态孔洞
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:
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 Graph | metadata.openGraph |
| 结构化数据 | 页面中输出 <script type="application/ld+json"> |
| Sitemap | app/sitemap.ts 导出 sitemap() 函数 |
| Robots | app/robots.ts 导出 robots() 函数 |
| Canonical URL | metadata.alternates.canonical |
| 多语言 hreflang | metadata.alternates.languages |
Q13: generateStaticParams 的作用是什么?
答案:
generateStaticParams 用于在构建时为动态路由生成静态页面(类似 Pages Router 的 getStaticPaths)。
// 构建时生成所有博客文章的静态页面
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 → 页面内容
// 路由切换时自动显示
export default function Loading() {
return <div className="skeleton">加载中...</div>;
}
'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。
const config = {
output: 'standalone', // 生成独立部署包
};
export default config;
# 阶段 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 | 无 | 纯静态站点 |