Next.js与SSR

What — 是什么

Next.js 是 Vercel 开发的 React 全栈框架,支持 SSR(服务端渲染)、SSG(静态生成)、ISR(增量静态再生)、CSR(客户端渲染)和 Streaming SSR(流式渲染),解决 SPA 的 SEO 和首屏性能问题,并提供基于 React Server Components 的 App Router 路由系统。

渲染模式详解

模式渲染时机数据实时性服务器负载首屏速度SEO适用场景
SSR每次请求时实时较快极好个性化页面、实时数据看板
SSG构建时固定极快极好博客、文档、营销页
ISR构建时 + 定时刷新准实时极快极好电商商品页、新闻列表
CSR客户端运行时实时后台管理、仪表盘
Streaming SSR请求时流式返回实时快(渐进)复杂页面、慢数据源

渲染流程对比:

SSR:  请求 → 服务端渲染完整 HTML → 返回 → Hydration → 可交互
SSG:  构建 → 生成静态 HTML → CDN 缓存 → 请求直接返回 → Hydration
ISR:  构建 → 静态 HTML + 定时触发 → 过期后后台重新生成 → 新请求返回新页面
CSR:  请求 → 返回空壳 HTML → 下载 JS → 客户端渲染 → 可交互
Streaming SSR: 请求 → 流式返回 HTML 片段 → 逐步 Hydration → 可交互

App Router vs Pages Router

Next.js 13+ 引入了全新的 App Router,与传统的 Pages Router 并存:

维度App RouterPages Router
目录app/pages/
路由定义文件系统(page.tsx)文件系统(自动映射)
布局嵌套 Layout(不重新渲染)单一 _app + _document
数据获取async/await(Server Component)getServerSideProps/getStaticProps
组件模型Server Component 优先全部为 Client Component
加载状态loading.tsx + Suspense手动处理
错误处理error.tsx_error.tsx
API 路由route.ts(Route Handlers)pages/api/
流式渲染内置支持不支持
状态推荐方案稳定方案

App Router 是 Next.js 的未来方向,Pages Router 仍然支持但不推荐新项目使用。

App Router 核心文件约定

App Router 通过文件名约定定义路由行为:

文件名用途说明
page.tsx页面组件定义路由 UI,是路由的唯一入口
layout.tsx布局组件共享布局,导航时不重新渲染
loading.tsx加载状态自动包裹 Suspense,流式加载时显示
error.tsx错误边界捕获子组件错误,必须是 Client Component
not-found.tsx404 页面路由未匹配时显示
template.tsx模板组件类似 Layout 但导航时重新挂载
default.tsx并行路由默认并行路由未匹配时的回退 UI
route.tsAPI 路由处理 GET/POST 等 HTTP 请求
middleware.ts中间件请求前拦截,鉴权/重定向

Server Components vs Client Components

维度Server ComponentsClient Components
声明方式默认,无需声明文件顶部 "use client"
执行位置服务端客户端(浏览器)
JS 发送到客户端否(零 JS 体积)
状态管理不支持 useState/useReducer支持
副作用不支持 useEffect支持
浏览器 API不可用(window/document)可用
数据获取直接 async/await通过 useEffect 或 SWR/React Query
后端资源可直接访问数据库/文件系统不可
事件处理不支持 onClick/onChange支持
自定义 Hooks仅服务端 Hooks全部

组件选择决策树:

需要交互(onClick/onChange)? → Client Component
需要状态(useState/useReducer)? → Client Component
需要浏览器 API(window/localStorage)? → Client Component
需要 useEffect? → Client Component
需要直接访问数据库/文件系统? → Server Component
其余情况 → Server Component(默认)

路由系统详解

1. 动态路由

app/blog/[slug]/page.tsx    →  /blog/hello-world
app/shop/[...slug]/page.tsx →  /shop/a/b/c (Catch-all)
app/shop/[[slug]]/page.tsx  →  /shop 或 /shop/a/b/c (Optional Catch-all)

2. 路由组(Route Groups)

(groupName) 包裹目录,用于组织代码但不影响 URL:

app/(marketing)/about/page.tsx   →  /about
app/(marketing)/contact/page.tsx →  /contact
app/(shop)/products/page.tsx     →  /products

路由组可以让不同 URL 共享同一 Layout,也可以让同 URL 层级的页面拥有不同 Layout。

3. 并行路由(Parallel Routes)

@folder 定义同时渲染的多个插槽:

app/dashboard/@team/page.tsx
app/dashboard/@analytics/page.tsx
app/dashboard/layout.tsx  → 接收 team 和 analytics 两个插槽

4. 拦截路由(Intercepting Routes)

(.)/(..) 约定拦截路由跳转,实现模态框等效果:

app/feed/(.)photo/[id]/page.tsx  → 在 feed 页面内拦截 /photo/1 为模态框
app/photo/[id]/page.tsx          → 直接访问 /photo/1 时显示完整页面

数据获取与缓存

fetch 缓存策略:

选项说明等价 Pages Router
cache: 'force-cache'缓存请求,构建时获取一次getStaticProps
cache: 'no-store'不缓存,每次请求重新获取getServerSideProps
next: { revalidate: 60 }缓存 60 秒后重新验证ISR revalidate

revalidation 方式:

  • 时间基准(Time-based)export const revalidate = 60,页面级设置
  • 按需基准(On-demand):通过 revalidatePathrevalidateTag 手动触发

Server Actions

Server Actions 是 Next.js 13+ 引入的服务端函数,允许客户端直接调用服务端代码,无需手动编写 API:

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

import { revalidatePath } from 'next/cache';

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'); // 刷新缓存
}

核心特性:

  • "use server" 标记服务端函数
  • 可直接在 <form action={}> 中使用
  • 自动处理 CSRF 防护
  • 配合 useActionState / useOptimistic 实现乐观更新
  • 调用后可触发 revalidation

内置优化

优化项组件/API说明
图片优化<Image>自动 WebP/AVIF、懒加载、响应式尺寸
字体优化next/font自动子集化、预加载、零布局偏移
脚本优化<Script>控制加载策略(lazy/onload/afterInteractive)
链接预取<Link>视口内链接自动预取
元数据metadata / generateMetadata自动生成 SEO 标签
静态资源next/static自动 CDN 缓存、文件名哈希

Why — 为什么

SSR/SSG/ISR/CSR 详细对比

维度SSRSSGISRCSR
SEO极好(完整 HTML)极好(完整 HTML)极好(完整 HTML)差(空壳 HTML)
FCP(首次内容绘制)较快(0.8-1.5s)极快(0.3-0.5s)极快(0.3-0.5s)慢(1.5-3s)
TTI(可交互时间)较快(1-2s)较快(0.5-1.5s)较快(0.5-1.5s)慢(2-4s)
服务器负载高(每次请求渲染)无(CDN 直接返回)低(仅过期时渲染)无(静态托管)
数据实时性实时固定准实时(revalidate 间隔)实时
CDN 友好
TTFB较高(服务端计算)极低极低极低
构建时间短(无预渲染)长(所有页面)中(仅首次)
适用页面比例10-20%40-60%20-30%10-20%

什么时候用 Next.js vs 纯 SPA

应该用 Next.js 的场景:

  • 内容需要被搜索引擎索引(博客、电商、新闻、文档站)
  • 首屏加载速度是核心指标(C 端产品、落地页)
  • 需要全栈能力(API Routes + 数据库)
  • 页面间共享布局,导航体验要求高
  • 需要国际化(i18n)且要 SEO 友好

应该用纯 SPA(Vite React)的场景:

  • 内部后台管理系统(无 SEO 需求)
  • 高交互应用(在线编辑器、实时协作)
  • 已有成熟 SPA 架构,迁移成本高
  • 团队对 SSR/RSC 概念不熟悉
  • 部署环境不支持 Node.js(仅静态托管)

App Router vs Pages Router 迁移考量

考量说明
渐进迁移App Router 和 Pages Router 可以共存,逐页迁移
数据获取getServerSideProps → Server Component async/await
布局系统_app + _document → 嵌套 layout.tsx
API 路由pages/api/app/api/route.ts(Route Handlers)
路由参数context.params → 函数参数 params
404 处理_error.tsxnot-found.tsx + error.tsx
中间件兼容,middleware.ts 无需修改
学习成本RSC 概念 + 文件约定需要时间适应

建议:新项目直接用 App Router;老项目可渐进迁移,从叶子路由开始。

对比同类框架

维度Next.jsNuxt.jsRemixAstro
语言/框架ReactVueReact框架无关
SEO极好极好极好极好
首屏速度极快(零 JS 默认)
SSR支持支持支持支持
SSG支持支持支持核心能力
ISR支持支持不支持不支持
学习曲线
灵活性
全栈能力
社区生态最丰富丰富中等增长快

优缺点

  • 优点:
    • SSR/SSG/ISR/CSR 灵活选择,按页面决定最优策略
    • SEO 和首屏性能兼得
    • 内置图片/字体/脚本优化,零配置性能提升
    • Server Components 减少客户端 JS 体积
    • 全栈能力(Route Handlers + Server Actions + Middleware)
    • App Router 嵌套布局,导航不重新渲染
    • Streaming SSR 渐进加载,慢数据不阻塞整体
    • Vercel 部署一键托管,Edge Runtime 全球分布
  • 缺点:
    • 框架约束多,定制灵活性不如纯 SPA
    • Server/Client Component 边界易混淆,初学者常踩坑
    • 部署需 Node.js 环境(SSR 模式),静态托管仅限 SSG
    • App Router 仍在快速迭代,部分 API 可能变更
    • 构建时间随页面数增长,ISR 缓解但不完全解决
    • Hydration 在复杂页面仍有性能开销

How — 怎么用

快速上手

# 创建项目
npx create-next-app@latest my-app --app --typescript --tailwind --eslint

# 启动开发服务器
cd my-app && npm run dev

# 构建生产版本
npm run build && npm start

1. App Router 基础项目结构

my-app/
├── app/
│   ├── layout.tsx          # 根布局(必须包含 html + body)
│   ├── page.tsx            # 首页 /
│   ├── loading.tsx         # 全局加载状态
│   ├── error.tsx           # 全局错误边界
│   ├── not-found.tsx       # 404 页面
│   ├── globals.css         # 全局样式
│   ├── blog/
│   │   ├── layout.tsx      # 博客布局
│   │   ├── page.tsx        # /blog
│   │   └── [slug]/
│   │       └── page.tsx    # /blog/hello-world
│   ├── dashboard/
│   │   ├── layout.tsx
│   │   ├── loading.tsx     # dashboard 加载骨架
│   │   └── page.tsx        # /dashboard
│   └── api/
│       └── posts/
│           └── route.ts    # API Route: /api/posts
├── components/
│   ├── ui/                 # 通用 UI 组件
│   └── features/           # 业务组件
├── lib/
│   ├── db.ts               # 数据库连接
│   └── utils.ts            # 工具函数
├── middleware.ts            # 中间件
├── next.config.js          # Next.js 配置
├── tailwind.config.js
├── tsconfig.json
└── package.json

2. Server Component 数据获取

// app/blog/page.tsx — Server Component 默认,可直接 async
import Link from 'next/link';

interface Post {
    id: number;
    title: string;
    excerpt: string;
    createdAt: string;
}

// 默认 cache: 'force-cache',构建时获取(SSG)
async function getPosts(): Promise<Post[]> {
    const res = await fetch('https://api.example.com/posts', {
        cache: 'force-cache', // SSG
    });
    return res.json();
}

export default async function BlogPage() {
    const posts = await getPosts();

    return (
        <div className="max-w-4xl mx-auto py-8">
            <h1 className="text-3xl font-bold mb-6">博客列表</h1>
            <div className="space-y-4">
                {posts.map(post => (
                    <Link key={post.id} href={`/blog/${post.id}`}>
                        <article className="p-4 border rounded hover:shadow transition">
                            <h2 className="text-xl font-semibold">{post.title}</h2>
                            <p className="text-gray-600 mt-1">{post.excerpt}</p>
                            <time className="text-sm text-gray-400">{post.createdAt}</time>
                        </article>
                    </Link>
                ))}
            </div>
        </div>
    );
}

3. Client Component 交互

// components/PostEditor.tsx — 需要交互,标记为 Client Component
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

interface PostEditorProps {
    initialTitle?: string;
    initialContent?: string;
}

export function PostEditor({ initialTitle = '', initialContent = '' }: PostEditorProps) {
    const [title, setTitle] = useState(initialTitle);
    const [content, setContent] = useState(initialContent);
    const [submitting, setSubmitting] = useState(false);
    const router = useRouter();

    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setSubmitting(true);
        try {
            const res = await fetch('/api/posts', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ title, content }),
            });
            if (res.ok) {
                router.push('/blog');
                router.refresh(); // 刷新 Server Component 数据
            }
        } finally {
            setSubmitting(false);
        }
    };

    return (
        <form onSubmit={handleSubmit} className="space-y-4">
            <input
                value={title}
                onChange={e => setTitle(e.target.value)}
                placeholder="文章标题"
                className="w-full border rounded px-3 py-2"
            />
            <textarea
                value={content}
                onChange={e => setContent(e.target.value)}
                placeholder="文章内容"
                rows={10}
                className="w-full border rounded px-3 py-2"
            />
            <button
                type="submit"
                disabled={submitting}
                className="bg-blue-600 text-white px-6 py-2 rounded disabled:opacity-50"
            >
                {submitting ? '提交中...' : '发布文章'}
            </button>
        </form>
    );
}

4. 动态路由与 generateStaticParams

// app/blog/[slug]/page.tsx — 动态路由 + SSG
interface Post {
    title: string;
    content: string;
    author: string;
    createdAt: string;
}

async function getPost(slug: string): Promise<Post> {
    const res = await fetch(`https://api.example.com/posts/${slug}`, {
        cache: 'force-cache',
    });
    return res.json();
}

// 构建时预生成所有静态页面
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 async function generateMetadata({ params }: { params: { slug: string } }) {
    const post = await getPost(params.slug);
    return {
        title: `${post.title} - 我的博客`,
        description: post.content.slice(0, 160),
    };
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
    const post = await getPost(params.slug);

    return (
        <article className="max-w-3xl mx-auto py-8">
            <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
            <div className="text-gray-500 mb-8">
                {post.author} · {post.createdAt}
            </div>
            <div className="prose lg:prose-lg">{post.content}</div>
        </article>
    );
}

5. ISR 增量静态再生成

// app/products/[id]/page.tsx — ISR:构建时生成 + 定时刷新
interface Product {
    id: string;
    name: string;
    price: number;
    description: string;
    stock: number;
}

async function getProduct(id: string): Promise<Product> {
    const res = await fetch(`https://api.example.com/products/${id}`, {
        next: { revalidate: 60 }, // 每 60 秒重新验证
    });
    return res.json();
}

// 页面级 ISR 配置
export const revalidate = 60;

export async function generateStaticParams() {
    const products = await fetch('https://api.example.com/products').then(r => r.json());
    return products.map((p: Product) => ({ id: p.id }));
}

export default async function ProductPage({ params }: { params: { id: string } }) {
    const product = await getProduct(params.id);

    return (
        <div className="max-w-4xl mx-auto py-8">
            <h1 className="text-3xl font-bold">{product.name}</h1>
            <p className="text-2xl text-red-600 mt-2">&yen;{product.price}</p>
            <p className="text-gray-600 mt-4">{product.description}</p>
            <p className="mt-4">
                库存:{product.stock > 0 ? `${product.stock} 件` : '已售罄'}
            </p>
        </div>
    );
}

按需 Revalidation(API 触发):

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
    const body = await request.json();

    // 方式一:按路径刷新
    revalidatePath(`/products/${body.id}`);

    // 方式二:按标签刷新(fetch 时设置 tags)
    revalidateTag('products');

    return NextResponse.json({ revalidated: true, now: Date.now() });
}
// 在 fetch 中使用 tag
const res = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] }, // 可被 revalidateTag('products') 刷新
});

6. Server Actions 表单处理

// app/posts/new/page.tsx — Server Actions 处理表单
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

// Server Action
async function createPost(formData: FormData) {
    'use server';

    const title = formData.get('title') as string;
    const content = formData.get('content') as string;

    if (!title || !content) return;

    await db.post.create({
        data: { title, content, authorId: 'current-user' },
    });

    revalidatePath('/posts'); // 刷新文章列表缓存
    redirect('/posts');       // 重定向到列表页
}

export default function NewPostPage() {
    return (
        <div className="max-w-2xl mx-auto py-8">
            <h1 className="text-2xl font-bold mb-6">发布新文章</h1>
            <form action={createPost} className="space-y-4">
                <div>
                    <label className="block text-sm font-medium mb-1">标题</label>
                    <input
                        name="title"
                        className="w-full border rounded px-3 py-2"
                        placeholder="输入文章标题"
                    />
                </div>
                <div>
                    <label className="block text-sm font-medium mb-1">内容</label>
                    <textarea
                        name="content"
                        rows={8}
                        className="w-full border rounded px-3 py-2"
                        placeholder="输入文章内容"
                    />
                </div>
                <button
                    type="submit"
                    className="bg-blue-600 text-white px-6 py-2 rounded"
                >
                    发布
                </button>
            </form>
        </div>
    );
}

Server Actions + useActionState(带状态反馈):

// components/PostForm.tsx
'use client';

import { useActionState } from 'react';

async function submitPost(prevState: { message: string }, formData: FormData) {
    'use server';

    const title = formData.get('title') as string;
    if (!title) return { message: '标题不能为空' };

    await db.post.create({ data: { title } });
    revalidatePath('/posts');
    return { message: '发布成功!' };
}

export function PostForm() {
    const [state, formAction] = useActionState(submitPost, { message: '' });

    return (
        <form action={formAction}>
            <input name="title" className="border rounded px-3 py-2" />
            {state.message && <p className="text-sm mt-2">{state.message}</p>}
            <button type="submit" className="ml-2 bg-blue-600 text-white px-4 py-2 rounded">
                发布
            </button>
        </form>
    );
}

7. Middleware 中间件(鉴权、重定向)

// middleware.ts — 位于项目根目录或 src/ 目录
import { NextRequest, NextResponse } from 'next/server';

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

    // 1. 鉴权检查
    const token = request.cookies.get('auth-token')?.value;
    if (pathname.startsWith('/dashboard') && !token) {
        const loginUrl = new URL('/login', request.url);
        loginUrl.searchParams.set('callbackUrl', pathname);
        return NextResponse.redirect(loginUrl);
    }

    // 2. 已登录用户访问登录页,重定向到仪表盘
    if (pathname === '/login' && token) {
        return NextResponse.redirect(new URL('/dashboard', request.url));
    }

    // 3. 添加自定义请求头
    const response = NextResponse.next();
    response.headers.set('x-request-id', crypto.randomUUID());
    return response;

    // 4. 地区重定向(i18n)
    // const country = request.geo?.country || 'CN';
    // if (pathname === '/') {
    //     return NextResponse.redirect(new URL(`/${country.toLowerCase()}`, request.url));
    // }
}

// 匹配配置 — 只对这些路径运行中间件
export const config = {
    matcher: [
        '/dashboard/:path*',
        '/login',
        '/api/:path*',
    ],
};

8. 嵌套布局与 Loading 状态

// app/layout.tsx — 根布局
import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
    title: '我的应用',
    description: 'Next.js App Router 示例',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="zh-CN">
            <body>
                <nav className="border-b px-6 py-3">
                    <a href="/">首页</a>
                    <a href="/blog">博客</a>
                    <a href="/dashboard">仪表盘</a>
                </nav>
                {children}
            </body>
        </html>
    );
}
// app/dashboard/layout.tsx — 嵌套布局
export default function DashboardLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <div className="flex">
            <aside className="w-64 border-r p-4">
                <nav>
                    <a href="/dashboard/overview">概览</a>
                    <a href="/dashboard/analytics">分析</a>
                    <a href="/dashboard/settings">设置</a>
                </nav>
            </aside>
            <main className="flex-1 p-6">{children}</main>
        </div>
    );
}
// app/dashboard/loading.tsx — 自动 Suspense 骨架屏
export default function DashboardLoading() {
    return (
        <div className="animate-pulse space-y-4">
            <div className="h-8 bg-gray-200 rounded w-1/3" />
            <div className="h-4 bg-gray-200 rounded w-2/3" />
            <div className="grid grid-cols-3 gap-4">
                {[1, 2, 3].map(i => (
                    <div key={i} className="h-32 bg-gray-200 rounded" />
                ))}
            </div>
        </div>
    );
}

9. Image 图片优化

// components/HeroSection.tsx
import Image from 'next/image';

export function HeroSection() {
    return (
        <section className="relative h-[500px]">
            {/* 本地图片 — 自动获取宽高 */}
            <Image
                src="/hero.jpg"
                alt="首页横幅"
                fill
                priority          // 优先加载(首屏图片)
                sizes="100vw"
                className="object-cover"
            />

            {/* 远程图片 — 需配置域名 */}
            <Image
                src="https://cdn.example.com/photo.jpg"
                alt="远程图片"
                width={800}
                height={600}
                quality={85}       // 质量 1-100
                placeholder="blur" // 模糊占位
            />
        </section>
    );
}
// next.config.js — 远程图片域名配置
/** @type {import('next').NextConfig} */
const nextConfig = {
    images: {
        remotePatterns: [
            {
                protocol: 'https',
                hostname: 'cdn.example.com',
            },
        ],
        formats: ['image/avif', 'image/webp'],
    },
};

module.exports = nextConfig;

10. Route Handlers(API 路由)

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';

// GET /api/posts
export async function GET(request: NextRequest) {
    const searchParams = request.nextUrl.searchParams;
    const page = searchParams.get('page') || '1';
    const limit = searchParams.get('limit') || '10';

    const posts = await db.post.findMany({
        skip: (Number(page) - 1) * Number(limit),
        take: Number(limit),
    });

    return NextResponse.json({ posts, page: Number(page) });
}

// POST /api/posts
export async function POST(request: NextRequest) {
    const body = await request.json();
    const { title, content } = body;

    if (!title) {
        return NextResponse.json({ error: '标题必填' }, { status: 400 });
    }

    const post = await db.post.create({ data: { title, content } });
    return NextResponse.json(post, { status: 201 });
}

常见问题与踩坑

问题原因解决方案
”use client” 组件中用了服务端 APIClient Component 不运行在服务端数据在 Server Component 获取,通过 props 传递
useState 在 Server Component 报错Server Component 无状态拆成 Client Component 或提升状态
构建慢SSG 页面过多用 ISR 延迟生成,或动态路由
hydration mismatchSSR 和 CSR 渲染结果不一致避免在渲染中使用 Date.now()/Math.random()
Image 组件远程图片 403未配置 remotePatternsnext.config.js 添加域名白名单
Server Action 无响应忘记 'use server' 声明函数顶部添加 'use server'
Middleware 中读数据库Middleware 运行在 Edge Runtime用 Cookies/Headers 判断,数据库查询放 Server Component
布局刷新导致状态丢失Layout 导航时保持不重新挂载需要重置状态的场景用 template.tsx
fetch 在开发模式重复调用React StrictMode 双重渲染仅开发环境出现,生产环境正常
动态路由 params 类型报错Next.js 15+ params 是 Promise使用 await params 获取参数

最佳实践

  • 默认用 Server Component,交互部分才用 "use client"
  • 数据获取在 Server Component 中直接 async/await,避免 useEffect 瀑布
  • loading.tsx + Streaming 提升感知性能
  • 静态内容用 SSG,动态内容用 SSR/ISR
  • Server Actions 优先于手动 API Routes(简单表单场景)
  • 图片始终用 <Image> 组件,自动优化格式和尺寸
  • 布局嵌套不要过深,3 层以内为宜
  • generateStaticParams 预生成热门页面,减少服务器压力
  • Middleware 保持轻量,仅做鉴权和重定向,不做重计算
  • 合理使用 revalidate 值,避免全站设为 0(变成纯 SSR)

面试题

Q1: SSR、SSG、CSR 三种渲染模式有什么区别?各适用什么场景?

SSR 每次请求时服务端渲染 HTML,数据实时但服务器压力大,适用于个性化页面和实时数据;SSG 构建时生成静态 HTML,速度最快但内容固定,适用于博客/文档/营销页;CSR 客户端加载 JS 后渲染,SEO 差、首屏慢但交互灵活,适用于后台管理系统。ISR 是 SSG 的增强版,通过 revalidate 间隔实现准实时更新,适用于电商商品页等需要兼顾性能和时效的场景。

Q2: SSR 和 SSG 的区别?各适用什么场景?

SSR 每次请求时在服务端实时渲染 HTML,数据始终是最新的,但服务器负载高、TTFB 较长;SSG 在构建时一次性生成静态 HTML,CDN 直接返回,速度极快且服务器零负载,但数据更新需要重新构建。SSR 适用于用户个性化仪表盘、实时股票行情等数据频繁变化且需要 SEO 的场景;SSG 适用于博客文章、产品介绍、文档站等内容稳定、更新频率低的场景。ISR 是两者折中,用 revalidate 定时刷新。

Q3: getServerSideProps 和 getStaticProps 的区别是什么?(Pages Router)

getServerSideProps 每次请求时在服务端执行,获取实时数据,适用于动态页面;getStaticProps 构建时执行一次,生成静态 HTML,配合 revalidate 可实现 ISR 增量更新,适合内容不频繁变化的页面。在 App Router 中,两者统一为 fetch 的 cache 选项:cache: 'no-store' 等价 getServerSideProps,cache: 'force-cache' 等价 getStaticProps。

Q4: 什么是 Hydration?Hydration Mismatch 是怎么产生的?

Hydration 是服务端渲染的 HTML 在客户端被 React”激活”,绑定事件和状态的过程。Mismatch 产生于 SSR 和 CSR 渲染结果不一致,常见原因:渲染中使用了 Date.now()Math.random()、浏览器 API(window)或依赖了客户端状态。解决方案:将使用浏览器 API 的逻辑移入 useEffect(仅在客户端执行),或使用 suppressHydrationWarning 抑制已知差异。

Q5: Next.js 的 Server Components 和 Client Components 有什么区别?

Server Component 在服务端执行,可直接访问数据库/文件系统,不发送 JS 到客户端,支持 async/await 但不支持 useState/useEffect/onClick;Client Component 通过 "use client" 声明,在浏览器执行,支持状态管理和事件处理但会增加客户端 JS 体积。App Router 下组件默认是 Server Component,只有需要交互的组件才标记为 Client Component。关键原则:Server Component 可以导入 Client Component,但 Client Component 不能导入 Server Component(可通过 props 传递 JSX)。

Q6: 什么是 ISR?解决了什么问题?

ISR(Incremental Static Regeneration,增量静态再生)是 Next.js 的渲染策略,在 SSG 基础上增加了定时重新验证能力。通过 revalidate 设置间隔秒数,过期后后台重新生成页面,新请求仍返回旧缓存,生成完成后下次请求返回新页面。ISR 解决了 SSG 内容更新需要全站重新构建的问题,也避免了 SSR 的高服务器负载,实现了静态页面的”准实时”更新,特别适合电商商品页、新闻列表等内容更新频率中等、流量较大的页面。

Q7: Next.js 的 Middleware 可以做什么?

Middleware 是运行在 Edge Runtime 的请求拦截器,在请求到达页面之前执行。主要用途:1)鉴权 — 检查 Cookie/Token,未登录重定向到登录页;2)重定向 — 根据条件跳转(如地区、语言、设备类型);3)请求改写 — URL Rewrite 不改变浏览器地址栏但返回不同内容;4)添加自定义 Headers — 如请求 ID、CORS 头;5)A/B 测试 — 根据 Cookie 分配不同页面版本。注意 Middleware 运行在 Edge Runtime,不能访问 Node.js API(如 fs、数据库),应保持轻量。

Q8: App Router 的 Layout 和 Template 有什么区别?

Layout 在导航时保持不重新挂载,共享状态和副作用保持不变,适合全局导航栏、侧边栏等持久化 UI;Template 在导航时重新挂载,会重新执行 useEffect 和重置状态,适合需要在路由切换时重置的场景(如页面进入动画、表单状态重置)。默认优先用 Layout,只有需要重新挂载行为时才用 Template。


相关链接: