Next.js 面试高频与场景题
Next.js 面试题分三类:基础概念(构建工具 / 渲染模式)、架构设计(App Router / RSC 边界)、场景题(综合应用)。本篇按难度梯度整理高频题 + 答题框架。
一、基础概念题
❓ 题 1:Next.js 15 的渲染模型是什么?和传统 React SPA 有什么不同?
答题框架:
传统 SPA:
- 所有组件都是 Client Component
- 浏览器拿到空 HTML,下载 JS,React 渲染
- 首屏慢、SEO 差
Next.js 15:
- 默认 Server Component + RSC
- 服务端跑完组件 → 序列化 RSC Payload → 浏览器
- 浏览器只 hydrate Client Component
- 首屏快、bundle 小
关键术语:Server Components、Client Components、RSC Payload、Streaming SSR、Suspense。
❓ 题 2:Server Component 和 Client Component 的区别?什么场景用什么?
答题框架:
| 维度 | Server Component | Client Component |
|---|---|---|
| 声明 | 默认 | "use client" |
| 能否直连 DB | ✅ | ❌(不安全) |
| 能否 useState | ❌ | ✅ |
| 打包 | 服务端跑完即丢 | 发到浏览器 + hydrate |
原则:能 Server 就别 Client。只在需要交互的最小组件上加 "use client"。
❓ 题 3:解释 RSC Payload。SSR 的 HTML 和 RSC 的 Payload 有什么不同?
答题框架:
- SSR HTML:
<div>...</div>,浏览器直接渲染 - RSC Payload:类似 JSON 的特殊格式,包含组件树结构 + 已序列化的 props + Client Component 引用
关键:RSC Payload 是中间表示,浏览器解析后既能渲染 HTML,又能识别 Client Component 边界。
❓ 题 4:Next.js 14/15 引入的 Server Actions 是什么?和 API Route 的区别?
答题框架:
- API Route:手写
app/api/.../route.ts,客户端用 fetch 调用 - Server Actions:
"use server"标记的函数,直接当函数调,自动处理序列化、CSRF、加密
Server Actions 优势:
- 不用写 fetch
- 自动 CSRF 保护(只接受 POST)
- 自动加密 action ID
- 配合
useActionState拿到 pending + error
❓ 题 5:App Router 相对 Pages Router 的优势?
答题框架:
| 维度 | Pages Router | App Router |
|---|---|---|
| 布局 | 手动嵌套 | 文件系统嵌套 layout |
| 默认渲染 | Client | Server Component |
| Loading | 手动写 | loading.tsx 自动 |
| 流式 SSR | 不支持 | 原生 Streaming |
| API | pages/api | app/api + Route Handlers |
结论:新项目用 App Router。
❓ 题 6:什么是 Streaming SSR?解决什么问题?
答题框架:
- 传统 SSR 一次返回完整 HTML,慢的部分会阻塞整体
- Streaming SSR 用
renderToPipeableStream,先把外层 HTML 推给浏览器,慢的部分用 Suspense 兜底 - 配合
loading.tsx,用户立刻能看到骨架屏
关键 API:onShellReady 立刻 flush,onAllReady 等待所有 Suspense。
二、架构设计题
❓ 题 7:如何设计一个「商品详情页」?SSG / SSR / ISR / RSC 怎么选?
答题框架:
Q1: 商品数量级?
├─ 千级(精品电商) → SSG,构建时全量生成
├─ 万级 → ISR + 预生成热门
└─ 百万级(大平台) → ISR + 动态参数 + on-demand revalidate
Q2: 价格 / 库存是否实时?
├─ 不要求实时(缓存分钟级 OK) → ISR
└─ 要求实时(不能有延迟) → SSR
最终方案(典型电商):
// app/products/[id]/page.tsx
export async function generateStaticParams() {
// 预生成热门 1000 个
const hot = await db.product.findMany({ orderBy: { sales: "desc" }, take: 1000 });
return hot.map(p => ({ id: String(p.id) }));
}
export const revalidate = 60; // 60s 增量重新验证
export const dynamicParams = true; // 其他动态参数也走 ISR
export default async function ProductPage({ params }) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
return <ProductDetail product={product} />;
}
❓ 题 8:怎么优化 Next.js 首屏加载速度?
答题框架(五步法):
- 度量:Lighthouse / Vercel Analytics / Web Vitals
- 减少 JS 体积:
- 默认 Server Component(零 JS 下发)
- 动态 import 大组件:
const Chart = dynamic(() => import("./Chart")) - shadcn/ui 按需复制(不引整个库)
- 静态化:
- 静态内容走 SSG / ISR
- 客户端组件用
next/dynamic懒加载
- 资源优化:
next/image自动优化(webp、懒加载、srcset)next/font自托管字体(避免 FOIT/FOUT)
- 缓存:
- CDN 缓存静态页面
- Server Action 加 idempotency 防重复
加分项:
- 用
loading.tsx+ Streaming 让用户先看到骨架屏 - 关键资源
preload/preinit - 第三方脚本用
next/script的lazyOnload策略
❓ 题 9:RSC 的边界规则是什么?为什么函数不能传给 Client Component?
答题框架:
边界规则:
- Server Component 可 import Client Component(传 props)
- Client Component 不能 import Server Component
- 未声明
"use client"的组件是通用组件,可被两种父组件消费 - 跨边界传 props 必须是可序列化的
为什么函数不能传?
RSC Payload 是静态序列化格式(类似 JSON),函数没有序列化表示。如果允许传函数:
- 服务端无法把函数体编码到 Payload
- 客户端拿到函数引用也无法执行(代码不在客户端)
- 安全风险(客户端执行服务端函数 = 任意代码执行)
例外:Server Actions 是个特殊通道(经过加密 ID 映射),其他函数都不行。
❓ 题 10:Server Actions 在生产环境如何保证安全?
答题框架:
- Action ID 加密:每个 action 编译时生成唯一 ID,不暴露函数体(用户抓包看不到代码)
- POST-only:Server Actions 只接受 POST 请求(天然防 CSRF 探测)
- SameSite cookies:浏览器自动不发送第三方 cookie
- Origin 校验:Next.js 校验
Origin头,不匹配直接拒绝 - 参数校验:必须配 Zod 校验(攻击者改 formData 没商量)
最佳实践:
"use server";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1).max(50),
email: z.string().email(),
});
export async function updateProfile(formData: FormData) {
const parsed = Schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { error: parsed.error.flatten() };
// ... 真正更新逻辑
}
三、场景综合题
❓ 题 11:电商秒杀页面怎么设计?
场景:1000 件商品,10 万人同时抢购,要求不超卖、响应快。
答题框架:
1. 渲染层(Next.js):
- 商品页用 SSG/ISR 静态化
- 抢购按钮实时库存用 Client Component 拉(轮询 / SSE)
- Loading + Streaming 减少等待
2. Server Action 处理下单:
- 校验登录态(cookie)
- 调用后端服务的库存扣减 API(不直连 DB)
- 幂等性:传 idempotency key 防重复
3. 服务端设计(不在 Next.js 范畴):
- Redis 预扣减(DECR 原子操作)
- MQ 异步生成订单
- 前端只轮询订单状态
Next.js 角度:用 RSC 静态化商品页 + Server Actions 提交订单 + Client Component 轮询状态。
❓ 题 12:博客系统怎么设计评论功能?
答题框架:
// 评论组件 —— 用 Server Actions 提交
// app/comments/CommentForm.tsx
"use client";
import { useActionState } from "react";
import { submitComment } from "@/app/actions/comment";
export function CommentForm({ postId }) {
const [state, action, isPending] = useActionState(
async (prev, fd) => {
const result = await submitComment(postId, fd);
return result ?? prev;
},
null,
);
return (
<form action={action}>
<textarea name="content" required />
<button disabled={isPending}>{isPending ? "提交中..." : "发布"}</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
// app/actions/comment.ts
"use server";
import { revalidatePath } from "next/cache";
export async function submitComment(postId, formData) {
const content = formData.get("content");
if (!content) return { error: "内容不能为空" };
await db.comment.create({ data: { postId, content } });
revalidatePath(`/posts/${postId}`); // ← 关键:刷新评论列表
}
关键点:
- Server Actions 提交
revalidatePath让评论列表自动刷新- 客户端拿到
isPending做 loading
❓ 题 13:怎么实现「登录后自动跳回原页面」?
答题框架:
// middleware.ts
import { NextResponse } from "next/server";
export function middleware(req) {
const token = req.cookies.get("token");
const isAuthed = !!token;
const isLoginPage = req.nextUrl.pathname === "/login";
const protectedPath = req.nextUrl.pathname.startsWith("/dashboard");
if (protectedPath && !isAuthed) {
// 关键:把当前 URL 编码到 redirect 参数
const url = new URL("/login", req.url);
url.searchParams.set("redirect", req.nextUrl.pathname);
return NextResponse.redirect(url);
}
return NextResponse.next();
}
// app/login/page.tsx
"use client";
import { useSearchParams, useRouter } from "next/navigation";
export default function LoginPage() {
const searchParams = useSearchParams();
const router = useRouter();
async function handleLogin() {
await login();
// 跳回原页面,没有就回首页
const redirect = searchParams.get("redirect") || "/";
router.push(redirect);
}
return <button onClick={handleLogin}>登录</button>;
}
❓ 题 14:怎么排查 Next.js 应用 Hydration mismatch 错误?
答题框架:
常见原因:
- 服务端 / 客户端渲染结果不一致:
Date.now()/Math.random()window/document引用- 时间格式(locale 不同)
- 浏览器扩展(翻译插件、广告拦截)改了 DOM
- HTML 不合法(如
<p>嵌套<div>)
排查方法:
// 1. 用 suppressHydrationWarning 暂时屏蔽(不推荐长期使用)
<div suppressHydrationWarning>{new Date().toLocaleString()}</div>
// 2. 拆分动态内容到 Client Component
"use client";
function ClientOnlyTime() {
const [time, setTime] = useState<string>("");
useEffect(() => {
setTime(new Date().toLocaleString());
}, []);
return <div>{time}</div>;
}
// 3. 用 useId 替代随机 id
const id = useId(); // SSR / CSR 唯一且一致
❓ 题 15:Next.js 应用怎么实现「国际化」?
答题框架:
方案 1:路由级 i18n(推荐):
app/
├─ [locale]/
│ ├─ layout.tsx
│ ├─ page.tsx
│ └─ about/page.tsx
// next.config.ts
const i18n = {
locales: ["zh", "en", "ja"],
defaultLocale: "zh",
};
方案 2:next-intl 库:
import { useTranslations } from "next-intl";
export default function Page() {
const t = useTranslations("Home");
return <h1>{t("title")}</h1>;
}
方案 3:i18next + react-i18next(客户端方案,配合 localStorage 缓存用户偏好)。
加分项:RSC 下用 getTranslations 服务端读 messages,比客户端方案首屏更快。
四、面试结尾反问
准备 2~3 个反问,展示你深入思考。
- 你们项目用的是 App Router 还是 Pages Router?为什么?
- RSC 在你们项目落地的最大收益是什么?踩过哪些坑?
- 你们的 ISR 失效策略是怎么设计的?基于时间还是事件?
- Turbopack 在你们生产 build 用过吗?稳定性如何?