SSR/SSG 渲染模式全景:Next.js App Router、流式渲染与岛屿架构

引言 渲染模式决定了用户看到第一个字节的速度、SEO 的可见性与服务器的成本。CSR、SSR、SSG、ISR 各有适用边界,Next.js App Router 的 React Server Components 与流式渲染又进一步模糊了传统分类。本文系统梳理渲染模式全景,深入 Next.js、Nuxt、Astro 等框架的实现机制,并给出按业务场景的选型建议。

引言

渲染模式决定了用户看到第一个字节的速度、SEO 的可见性与服务器的成本。CSR、SSR、SSG、ISR 各有适用边界,Next.js App Router 的 React Server Components 与流式渲染又进一步模糊了传统分类。本文系统梳理渲染模式全景,深入 Next.js、Nuxt、Astro 等框架的实现机制,并给出按业务场景的选型建议。文中涉及的 Next.js App Router、generateStaticParams、streaming、Astro 岛屿、Nitro 均为真实生产能力。


一、渲染模式全景:CSR / SSR / SSG / ISR

1.1 四种模式的定义

模式渲染时机HTML 来源首屏 TTFB动态性适用场景
CSR浏览器运行时空壳 + JS 渲染快(但空白)高后台、内网工具
SSR每次请求时服务器动态生成慢高需要 SEO 的动态页
SSG构建时预生成静态文件最快低博客、文档、营销页
ISR构建时 + 按需静态 + 增量更新最快中内容周期更新的列表页

CSR(Client-Side Rendering)的典型问题是首屏白屏:浏览器先下载空的 HTML,再加载 JS bundle,最后渲染内容。SSR 让服务器在响应中直接返回渲染好的 HTML,解决 SEO 与首屏,代价是每个请求都消耗服务器 CPU。SSG 把渲染前置到构建期,得到的是纯静态文件,可由 CDN 直接服务,但对"随用户变化"的内容无能为力。

1.2 模式不是二选一

现代框架强调混合渲染:同一应用中不同路由可以使用不同模式。Next.js 在 App Router 中让开发者按路由声明渲染策略,这在第六节会展开。先理解一个关键权衡维度——新鲜度 vs 速度:

需求最佳模式
内容几乎不变,访问量大SSG + CDN
内容会变但可接受延迟ISR(revalidate)
每次访问都不同(个性化)SSR
纯交互应用,无需 SEOCSR
内容大、可拆块SSR + 流式渲染

渲染模式的本质是在「新鲜度」与「速度」之间定价:内容越静态、越值得被 CDN 缓存;越个性化,越必须走服务器动态渲染。


二、Next.js App Router:Server Components 与新模型

2.1 App Router 的核心概念

Next.js 13 引入 App Router,以 app/ 目录为根,page.js、layout.js、loading.js、error.js 等文件约定路由。与旧 Pages Router 最大的不同是:App Router 中的组件默认是 Server Components(RSC)——它们在服务器上执行,不向浏览器发送 JS,直接产出 HTML。

// app/products/page.tsx —— 服务端组件
import { db } from '@/lib/db';

export default async function ProductsPage() {
  // 直接 await 数据库查询,数据在服务端拿到
  const products = await db.product.findMany({ take: 20 });

  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>
          <span>{p.name}</span>
          <span>{p.price}</span>
        </li>
      ))}
    </ul>
  );
}

Server Components 里可以写 async function 并直接 await 数据源——没有 useEffect、没有 loading state,数据就在 HTML 里。浏览器收到的是一段已经渲染好的内容,无需水合的 JS 逻辑(该组件不参与客户端交互时)。

2.2 客户端组件的显式声明

当组件需要交互(useState、事件)时,用 'use client' 指令显式标记为 Client Component:

// app/products/[id]/AddToCartButton.tsx
'use client';

import { useState } from 'react';

export function AddToCartButton({ productId }: { productId: string }) {
  const [added, setAdded] = useState(false);
  return (
    <button
      onClick={() => {
        setAdded(true);
        // 调用购物车 API
      }}
    >
      {added ? '已加入' : '加入购物车'}
    </button>
  );
}

2.3 缓存与重新验证

App Router 引入了 fetch 级缓存语义:默认 GET 请求会被缓存,通过 revalidate 或 cache: 'no-store' 控制新鲜度:

// app/products/page.tsx
export default async function Page() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 }, // 每 60 秒重新验证一次
  });
  const products = await res.json();
  // ...
}

这与 ISR 的语义一致:静态生成 + 后台增量更新。App Router 把"渲染模式"从页面级下沉到数据级,这是架构上的重要变化。


三、流式渲染与 Suspense

3.1 流式渲染:TTFB 不再等待最慢的块

传统 SSR 需要整页数据全部就绪才返回响应。流式渲染(Streaming)则先把 HTML 外壳发出去,各异步区块就绪后通过 <script> 块增量注入。Next.js 用 loading.js 与 <Suspense> 声明流式边界:

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

export default function Dashboard() {
  return (
    <div>
      <FastWidget /> {/* 立即渲染,随首包输出 */}
      <Suspense fallback={<div>慢速组件加载中…</div>}>
        <SlowWidget /> {/* 通过流式注入,不阻塞首包 */}
      </Suspense>
    </div>
  );
}

3.2 流式渲染的协议基础

流式渲染依赖 HTTP 的 chunked transfer 与浏览器的增量解析。服务器边渲染边 flush,浏览器边接收边显示。用户感知上的收益是首屏可交互时间提前:外壳先出现,慢数据区域用占位符,就绪后平滑替换。

指标传统 SSR流式 SSR
TTFB等待最慢数据立即返回外壳
FCP慢快
LCP受最慢区块影响各区块独立
复杂度低需管理 Suspense 边界

流式的代价是边界管理:Suspense 切分粒度太细会产生大量小响应块,反而增加解析开销;太粗则退化为整页等待。实践上按"视觉区块"划分即可。


四、Nuxt/Nitro:Vue 生态的服务端能力

4.1 Nuxt 的渲染选项

Nuxt 3 基于 Vite + Nitro 服务器引擎,提供 ssr、static、spa 三种部署模式,并在页面级支持 defineRouteRules 声明 ISR 式的缓存规则:

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,
  routeRules: {
    '/blog/**': { swr: 3600 },            // 每小时重新验证
    '/docs/**': { static: true },          // 纯静态
    '/admin/**': { ssr: false },           // 关闭 SSR,纯客户端
    '/api/**': { proxy: '/api/**' },       // API 转发
  },
});

4.2 Nitro 与通用 API 层

Nitro 是 Nuxt 的服务端引擎,也是 Nuxt 的通用 API 层——可以用 server/api/ 目录编写服务端函数,自动暴露为 API 路由。这意味着一套代码既能做 SSR,也能提供接口:

// server/api/products.get.ts
export default defineEventHandler(async (event) => {
  const { name } = getQuery(event);
  const products = await getProducts({ keyword: name });
  return products;
});

Nitro 支持部署到 Node、Serverless、Edge 等多种环境,routeRules 在不同环境映射为不同的缓存策略(CDN cache、ISR、SWR)。


五、岛屿架构:Astro 与 Partial Hydration

5.1 岛屿架构的动机

水合(Hydration)是 SSR 的隐性成本:服务器渲染了 HTML,浏览器还要为每个组件下载并执行 JS,把事件重新挂上去。对于以内容为主的页面,大部分组件根本没有交互,全部水合是巨大的浪费。**岛屿架构(Islands Architecture)**的核心是:默认零 JS,只有需要交互的"岛屿"组件才会水合。

Astro 是岛屿架构的代表实现。页面默认输出纯静态 HTML,交互组件通过 client:* 指令按需加载:

---
// src/pages/index.astro
import Header from '../components/Header.astro';
import CartButton from '../components/CartButton.jsx';
---

<Layout>
  <!-- 静态组件:不输出任何 JS -->
  <Header />

  <!-- 岛屿:只有这个按钮会水合并加载 JS -->
  <CartButton client:load />

  <!-- 进入视口时才加载 -->
  <LazyChart client:visible />
</Layout>

5.2 Partial Hydration 的收益

方案JS 体积水合成本适用内容页
全量 SSR + 全量水合全量 bundle所有组件否
岛屿架构仅交互组件仅岛屿是
流式 SSR + 局部水合渐进加载按区块中

岛屿架构非常适合内容驱动的站点:文档、博客、营销页、电商详情页。交互密集的复杂应用(如富文本编辑器密集的后台)则收益有限。Astro 也支持混合多框架:React、Vue、Svelte 组件可以共存于同一页面,各自作为岛屿水合。


六、水合(Hydration)与失效

6.1 水合的本质与常见错误

水合是"把服务器生成的 DOM 变成响应式 DOM"的过程:React 会比对服务器 HTML 与客户端首次渲染结果,若不一致则报 mismatch 错误。经典触发源:

  • 依赖 Date.now()、Math.random() 等非确定性值,服务器与客户端渲染结果不同。
  • 依赖浏览器 API(window、localStorage)在渲染期读取。
  • 第三方库未做 SSR 兼容。
// 错误:服务器与客户端首渲染不一致
export function Timestamp() {
  const now = new Date().toISOString(); // 服务端和客户端时间不同
  return <time>{now}</time>;
}

// 正确:挂载后再读取浏览器值
'use client';
import { useEffect, useState } from 'react';

export function Timestamp() {
  const [now, setNow] = useState('');
  useEffect(() => setNow(new Date().toISOString()), []);
  return <time>{now}</time>;
}

6.2 减少水合体积

水合体积与 JS 成正比,优化手段:

  • 尽量让组件保持为 Server Component,减少 'use client' 范围。
  • 使用 react-dom 的流式水合与渐进增强。
  • 对非首屏交互组件延迟加载(Next.js 的 dynamic 配合 ssr: false)。
import dynamic from 'next/dynamic';

// 只在客户端加载、且按需加载的弹窗
const ConfirmDialog = dynamic(() => import('./ConfirmDialog'), {
  ssr: false,
  loading: () => <div>加载弹窗…</div>,
});

七、SEO 与性能权衡

7.1 SEO 对渲染模式的硬性要求

搜索引擎爬虫(Googlebot、Bingbot)虽然已能执行 JavaScript,但对 SSR/SSG 生成的内容仍更有好感——HTML 直接携带内容与结构化数据,无需等待 JS 执行。需要 SEO 的页面应至少保证关键内容在首包 HTML 中可见:

  • SSR/SSG:内容天然在 HTML 中。
  • CSR:爬虫需执行 JS,索引不稳定且延迟。
  • 元信息(title、description、og 标签)应服务端生成,避免客户端后补。
// Next.js App Router 的 metadata 导出,服务端生成 SEO 元信息
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'SSR/SSG 渲染模式全景',
  description: '从 CSR 到岛屿架构的完整选型指南',
  openGraph: {
    title: 'SSR/SSG 渲染模式全景',
    type: 'article',
  },
};

7.2 性能指标权衡矩阵

业务类型首屏体验优先SEO 优先成本优先
电商详情页SSG/ISRSSR/SSGSSG + CDN
内容博客SSGSSGSSG
个性化仪表盘CSR/流式 SSR不需要CSR
动态社区流式 SSRSSRISR

性能上的铁律:TTFB 快的模式,动态性必然受限。SSG 的 TTFB 最快但内容在构建期冻结;SSR 动态性最强但每个请求都产生服务器负载。ISR 与流式渲染正是为了在这两端之间提供中间带。

一个反直觉的事实:SSR 不必然比 CSR 慢,也不必然比 SSG 快。真正的分水岭是「每请求的服务器成本」——SSR 的快只能通过分层缓存兑现。


八、缓存与 CDN

8.1 缓存策略分层

SSR/SSG 的规模化瓶颈是服务器负载与回源延迟,解决方案是缓存分层:

层缓存什么手段
CDN 边缘完整 HTML(静态/ISR 产物)CDN cache、stale-while-revalidate
应用层渲染结果/API 响应Redis、内存缓存
数据层数据库查询结果查询缓存

以 Vercel 为代表的平台把 ISR 缓存放到了 CDN 边缘:revalidate 时间内用户直接命中边缘缓存,到期后后台重新生成。自建时可用 s-maxage、stale-while-revalidate 控制:

// 服务端响应的缓存头控制
export async function GET() {
  const data = await fetchFeed();
  return new Response(JSON.stringify(data), {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
    },
  });
}

8.2 缓存失效的复杂度

缓存失效是缓存系统最难的部分。ISR 用"时间窗口"规避显式失效——内容变更后最多延迟一个 revalidate 周期可见。对"提交后立即可见"的场景(如评论发布),需要主动失效:

// Next.js 的按需重新验证
export async function POST(request) {
  // 提交新评论
  await createComment(await request.json());
  revalidatePath('/blog/hello-world'); // 立即重新生成该路径
  return Response.json({ ok: true });
}

缓存与动态内容的边界应尽早划清:哪些页面允许"最多延迟 N 秒可见",哪些必须实时。前者交给缓存,后者只能走 SSR 或客户端实时拉取。


九、选型建议与工程实践

9.1 按团队与项目选型

场景推荐理由
React 生态、SEO 敏感Next.js App RouterRSC + 流式 + ISR 一体化
Vue 生态Nuxt 3routeRules + Nitro 全栈
内容站、文档站Astro岛屿架构,零 JS 默认
纯后台、无需 SEOVite + React CSR最简单、开发体验最好
老项目渐进改造保留 CSR,局部上 SSG降低改造风险

9.2 迁移路线与常见陷阱

从 CSR 迁移到 SSR 的经典陷阱:

  1. 窗口对象不可用:把 window/document 的读取移到 useEffect 或守卫中。
  2. 水合 mismatch:非确定性渲染(时间、随机数)需延迟到客户端。
  3. 依赖体积:Node 环境打包需注意 Node 专属依赖被错误打进客户端 bundle。
  4. 数据库连接泄漏:SSR 每请求创建连接,需连接池与超时管理。

9.3 最后的实践建议

现代前端已经很少"整站选一种模式",而是按路由声明式地混合。Next.js 的 App Router、Nuxt 的 routeRules、Astro 的默认静态 + 岛屿,都在推动同一个方向:让"静态优先、按需动态"成为默认哲学。静态能覆盖的,绝不动态;动态必须的部分,尽量窄化到最小组件。

# 本地验证生产模式的构建行为
npm run build
npm run start

结语

SSR、SSG、ISR、流式渲染与岛屿架构,本质是同一道权衡题的不同解:在新鲜度、速度、成本之间找到业务最优平衡点。Next.js App Router 把渲染模式下沉到数据与路由粒度,Nuxt 通过 routeRules 声明式管理,Astro 用岛屿架构把默认 JS 降到零。选型的核心依据是内容的新鲜度需求与 SEO 敏感性,而不是对某个框架的偏好。

对大多数团队,我推荐从"SSG/ISR 优先 + 关键动态路由走流式 SSR"起步,用 CDN 承载静态内容,让服务器只处理真正的动态请求。渲染模式的演进方向始终清晰:让静态的东西更便宜,让动态的东西更精准。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. CSS 架构与样式方案:从方法论到现代 CSS 新特性
  2. 可访问性与国际化:WCAG 2.2、ARIA 与 i18n 工程实践
  3. 前端测试体系:从 Vitest 单元测试到 Playwright E2E 的完整落地