前言
最近在看一些AI产品时, 一些功能 + 官网一体的网站, 在切换页面的时候会原地罚站, 结合自己之前使用next的场景, 做了一些思考和分析
1. 概览:四种渲染模式
- SSR(Server-Side Rendering):每次请求由服务端渲染页面并返回 HTML → 客户端 hydrate。优点:SEO 与首屏体验好;缺点:每次都要等 server 渲染(感知延迟)。
- SSG(Static Site Generation):在构建时(build)预生成 HTML,部署到 CDN。优点:秒级响应,低延迟;缺点:不是实时数据(需要 re-build 或 ISR)。
- ISR(Incremental Static Regeneration)/ 过期重建:静态生成 + 可在运行时按策略重新生成(revalidate),兼顾静态性能与一定实时性。
- CSR(Client-Side Rendering):初始页面或壳子从服务端或静态文件获取,核心数据通过客户端 JS fetch 获取并渲染。优点:页面切换流畅(尤其在 SPA 内),对动态内容友好;缺点:首屏可能慢、SEO 不占优(可通过 prerender / dynamic rendering 补救)。
2. 各模式流程图
2.1 普通 SSR(无 Prefetch)
[用户点击链接]
│
▼
[客户端路由捕获]
│
▼
[向 Server 请求页面渲染(生成 HTML/JSON)]
│
▼
[服务端渲染完成并返回]
│
▼
[客户端接收并 hydrate]
│
▼
[页面可交互]
2.2 SSR + Prefetch
[浏览页面时 background prefetch 启动]
│
▼
[提前下载目标页面的 JSON/HTML/JS bundle]
│
[用户点击链接]
│
▼
[客户端直接使用已缓存的数据并 hydrate]
│
▼
[页面快速可交互 / 接近秒切换]
2.3 SSG 或 CDN 托管
[用户点击链接]
│
▼
[直接从 CDN 获取静态 HTML + JS]
│
▼
[几乎无需等待 server 渲染 → hydrate]
│
▼
[页面几乎瞬间可交互]
2.4 CSR
[用户点击链接]
│
▼
[路由在客户端切换(无请求 server 渲染页面)]
│
▼
[客户端 fetch 必要数据(API)并渲染]
│
▼
[页面流畅切换]
3. 为什么 SSR 会卡顿(——即便静态内容也会卡)
核心点:SSR 的“卡顿”来自三个叠加的环节:
- 网络往返(RTT):即便内容固定,客户端仍需向 Server 请求新页面渲染结果(JSON/HTML),这有一段网络延迟。
- 服务端渲染时间:服务端需要执行 Server Component / Vue 组件的渲染逻辑(模板渲染、组件执行、可能的同步数据处理),即便数据很少,这也需要 CPU 时间。
- 客户端 hydrate(或复用)开销:把静态 HTML 变成可交互的虚拟 DOM、绑定事件、运行客户端组件初始化逻辑,需要 JS 执行时间,复杂页面或大型 bundle 会明显增加耗时。
即使“页面不再依赖 server 数据”,只要渲染是在 server 端执行,前两项就不可避免;且 hydrate(第三项)在任何 SSR 路径上都存在。
4. Prefetch 能做什么 / 做不了什么(关键区分)
Prefetch 能做的:
- 提前拉取目标页面所需的 JS bundle、部分 JSON 数据或 HTML 片段,使得用户点击后不再需要完整的网络往返,显著降低感知延迟(尤其在网络稳定时)。
- 与 CDN / 浏览器缓存结合,把资源预先放到本地缓存,切换几乎瞬时。
- 提前加载第三方资源(例如字体、重要图片)。
Prefetch 做不了的:
- 完全消除服务端渲染时间:如果 server 必须在点击时执行不可缓存的动态逻辑,prefetch 无法消除这些实时计算。
- 消除 hydrate 的 JS 执行时间:即便资源已就绪,hydrate 仍需 JS 执行(除非页面设计成无需大量 hydrate)。
- 解决个性化且需要认证的场景:若页面渲染依赖用户权限或 session,提前预取公共版本可能不适合(或需特殊处理)。
5. CSR 在动态内容场景下的优势与局限
CSR 优势(切换体验层面)
- 页面切换几乎没有重新走 server 渲染的等待(单页应用的路由切换在客户端完成)。
- 对于高度动态或实时更新的 UI(仪表盘、聊天、实时数据),CSR 可以显著提升交互流畅度与切换体验。
- 与 SWR / React Query 配合,可做局部数据缓存与 stale-while-revalidate,从而保持数据及时性又不阻塞渲染。
CSR 局限
- 首屏渲染依赖客户端 JS:首屏可能慢,尤其在低端设备或慢网络下。对 SEO 不利(搜索引擎爬虫可能抓不到数据,虽现代搜索引擎已改进)。
- SEO 处理复杂:需要 prerender、动态渲染(dynamic rendering)或针对爬虫的 SSR fallback。
- 资源加载与安全:如果初始 bundle 很大,会影响首次加载体验。
6. SEO 与动态内容的权衡:常见场景与推荐方案
下面按典型业务场景给出建议和理由。
场景 A:营销型页面 / Landing page / blog 首页(SEO 为主)
推荐:SSG(静态生成)或 SSG + ISR 原因:SEO 与首屏性能最关键,静态生成能把 HTML 部署到 CDN,实现最快加载。ISR 可以在内容更新时按需刷新(例如文章更新、前端编辑器触发 revalidate)。
场景 B:电商商品详情页(SEO + 部分动态,如库存/价格)
推荐:混合策略
- 使用 SSG / ISR 生成大部分静态商品内容(SEO 友好)。
- 对于库存、价格、促销等实时信息使用 客户端 fetch(CSR 局部刷新)或通过 Edge 函数做快速 SSR 更新(cache short TTL + SWR)。 实现要点:页面 SSR/SSG 返回 SEO 内容与关键信息,客户端再 fetch 最新价格并显示(避免阻塞首屏)。
场景 C:搜索 / 筛选页面(高度动态,用户期望实时)
推荐:CSR(客户端渲染)为主,关键入口可 SSR/SSG
- 搜索结果对 SEO 不如商品详情重要(视业务而定)。
- 建议页面 shell SSR,但搜索结果由客户端 fetch,支持即时响应与分页/筛选。
- 可对热门搜索 / 热门关键词做 SSG 预渲染。
场景 D:用户仪表盘 / 私有授权页面(无需 SEO)
推荐:CSR(或 SSR 但主要靠客户端 fetch) 原因:页面个性化、需要认证,SSR 对 SEO 无帮助,CSR 可以保证切换流畅与交互即时。
场景 E:新闻站点(强 SEO + 频繁更新)
推荐:SSG + ISR
- 文章静态生成,结合 ISR 做近实时更新(revalidate 值根据业务决定)。
- 对重要突发新闻可以做 server-rendered fallback 或 on-demand revalidate。
7. 混合策略(Hybrid)与实现模式(Next.js / Nuxt 示例)
7.1 常用混合模式(设计思想)
- Static First + Client Patches:尽量 SSG,客户端补全实时数据。适用于电商商品页、博客。
- SSR for bots + CSR for humans(动态渲染):对爬虫采用 SSR/ prerender,而对普通用户采用 CSR;或对爬虫做 server-rendered snapshot。适用于内容差异大但需 SEO 的 SPA。
- Partial SSR + Streaming + Suspense:关键部分服务端渲染,次要(或重量级)块作为 Client Component 延迟加载并用骨架屏占位。适用于大型页面。
7.2 Next.js(App Router / Next 14)实战片段
静态 + 客户端补丁(商品页示例)
// app/product/[id]/page.tsx (Server Component)
import { fetchProduct } from '@/lib/api';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetchProduct(params.id); // 可使用 fetch(..., { next: { revalidate: 60 } })
return (
<>
<ProductSeo product={product} /> {/* SEO 内容 */}
<div>
<ProductMain product={product} /> {/* 大部分静态渲染 */}
{/* 客户端组件负责实时库存/价格 */}
<ClientStockAndPrice productId={params.id} />
</div>
</>
);
}
客户端补丁组件(Client Component)
'use client';
import useSWR from 'swr';
export default function ClientStockAndPrice({ productId }: { productId: string }) {
const { data } = useSWR(`/api/product/${productId}/live`, fetcher, { refreshInterval: 30000 });
if (!data) return <div>Loading price...</div>;
return <div>Price: {data.price} | Stock: {data.stock}</div>;
}
使用 ISR / revalidate(Next fetch)
// fetch in server component
const res = await fetch(`https://api.example.com/product/${id}`, { cache: 'force-cache', next: { revalidate: 60 } });
7.3 Nuxt / Vue(Nuxt 3)实现思路
- 使用
nitro的缓存 / prerender / routeRules 来配置静态化与 revalidation。 - 商品页静态生成 HTML(或 ISR),并在客户端用
useFetch或useAsyncData获取实时数据。
Nuxt 示例伪代码:
<script setup>
// server fetch for SEO content
const { data: product } = await useAsyncData('product', () => $fetch(`/api/product/${id}`), { server: true, initialCache: true })
</script>
<template>
<ProductSeo :product="product" />
<ProductMain :product="product" />
<ClientOnly>
<StockAndPrice :productId="id" />
</ClientOnly>
</template>
8. 性能优化清单(工程级落地方案)
8.1 网络与缓存
- 使用 CDN 托管静态资源(SSG 输出、JS bundle、图片)。
- 对 server 渲染结果设置合理的缓存策略:
Cache-Control,stale-while-revalidate。 - 对可缓存的 server 页面使用 ISR 或短 TTL 缓存(Edge Cache)。
- 使用 ETag / Conditional GET 减少带宽。
8.2 预取(Prefetch / Preload)
- 启用路由 prefetch(Next.js Link 默认在视口内 prefetch)。
- 对关键资源使用
<link rel="preload">(字体、首屏关键图片)。 - 对关键 API 做 background prefetch(例如用户可能下一步打开的资源)。
8.3 分包与延迟加载
- 动态 import 重组件(
dynamic()/defineAsyncComponent),把非首屏逻辑拆出。 - 避免一次性把大量 polyfills / 所有第三方库打进首屏 bundle。
- 使用 HTTP/2 或 HTTP/3 多路复用 + 按需加载。
8.4 Hydration 优化
- 减少客户端激活的 Client Components 数量(尽可能把可静态渲染的部分留给 server)。
- 使用 React 18 的 selective hydration、partial hydration(未来/实验工具)或 Vue 的 partial hydration 插件(根据生态)。
- 对大表格 / 列表使用虚拟化(Virtualized list)来减少 DOM 与 hydrate 开销。
8.5 数据策略
- 使用 SWR / React Query 做客户端缓存 + stale-while-revalidate。
- 服务端对慢接口做缓存层(Redis / in-memory / edge cache),减少每次渲染的延迟。
- 对可预测的更新(如计时器)使用 revalidate 与增量更新,而非每次 SSR。
8.6 用户与个性化处理
- 若页面高度个性化(基于用户权限),考虑:
- SSR 返回“通用内容”的静态版,客户端补个性化部分;或
- 使用 Edge functions 做快速 SSR(接近 CDN 延迟);或
- 对爬虫/SEO 做 prerender(服务端 snapshot),对真实用户做 CSR。
8.7 监控与度量
- 度量关键指标:TTFB、First Contentful Paint (FCP)、Time to Interactive (TTI)、Hydration time、Route change latency。
- 使用 Real User Monitoring(RUM)追踪路由切换的真实感知时间。
- 在 A/B 测试中验证 Prefetch、ISR、Client-first 改动对转化率和感知速度的影响。
9. 决策:什么时候选哪种方案
| 业务目标 | 优先级 | 推荐方案 | 说明 |
|---|---|---|---|
| SEO 首位(营销页面) | 高 | SSG + ISR | 静态 + 定时或触发重建,CDN 分发 |
| 商品详情(需 SEO + 实时库存) | 高 | SSG/ISR + Client fetch | 静态 SEO 内容 + 客户端补价/库存 |
| 搜索/筛选页(交互第一) | 中 | CSR(shell SSR 可选) | 客户端即时过滤/分页 |
| 用户仪表盘(私有) | 低 | CSR | 无 SEO,全部客户端渲染 |
| 新闻站点(频繁更新) | 高 | SSG + ISR(短 revalidate) | 文章静态,突发新闻可 on-demand revalidate |
| 高度个性化(推荐/个性化首页) | 中 | SSR 或 CSR 混合 | SSR 渲染模板 + Client 补个性化或 Edge SSR |
10. 案例解析
案例 A:电商商品页(SEO + 价格/库存实时)
- 方案:SSG 或 ISR 生成商品静态页,Client Component 获取实时库存/价格。
- 为什么:SEO 要求商品详情包含完整内容,S SR 会造成切换卡顿且不必要。实时字段用客户端 fetch 更新,保持用户看到最新数据而不阻塞首屏。
- 要点:
revalidate设置合理(比如 60s)对大部分商品足够;- 核心头部(title/meta)由 SSG 提供;
- 用 SWR 做客户端数据缓存并设置短 refreshInterval。
案例 B:搜索页(复杂筛选)
- 方案:CSR(客户端路由 + API 分页);首页搜索入口可做 SSR/SSG。
- 为什么:用户期望交互即时(筛选、分页),CSR 能做到几乎无卡顿。
- 要点:
- 搜索结果可做后端 API 支持 pagination;
- 对热门关键词做 SSG 以提升 SEO。
案例 C:新闻站(大量内容与 SEO)
- 方案:SSG + ISR + on-demand revalidate;
- 为什么:文章需要被索引,且更新频繁,需要靠 ISR 保持较高实时性。
- 要点:
- 当编辑器发布新文章时触发 on-demand revalidate;
- 热门新闻可触发即时 rebuild 或 server-rendered fallback。
案例 D:产品后台仪表盘(私有数据)
- 方案:CSR + API;可以做少量 SSR shell 用于首屏骨架。
- 为什么:仪表盘高度个性化且需要持续交互(拖拽、实时数据),CSR 最合适。
- 要点:
- 使用 websockets 或 SSE 提供实时数据;
- 初始 shell 可 SSR 减少白屏感,但数据由客户端加载。
11. 工程化落地计划 Checklist
- 梳理页面按 SEO/实时性/个性化分组(先分类再决策)。
- 优先 SSG/ISR 对于 SEO 重的页面,确定 revalidate 策略。
- 对需要实时数据的字段使用 Client fetch(仅替换动态区域)。
- 开启路由 prefetch(检查 Link / NuxtLink 配置与行为)。
- 引入 SWR / React Query 做客户端缓存与 revalidate。
- 对慢接口做 server side 缓存(Redis / edge),减少 server 渲染时间。
- 拆分大 bundle / 动态 import,减少 hydrate 成本。
- 实施 skeleton / Suspense 占位,优化感知速度。
- 使用 RUM 监控路由切换时间,设立报警阈值。
- 逐页 A/B 测试不同策略(SSR vs SSG+Client)对业务指标影响。
附录:常用代码示例速查(Next.js / Nuxt)
Next.js(App Router)Prefetch 与 revalidate
// Link 默认会在视口内 prefetch,如需手动:
import Link from 'next/link';
<Link href="/product/123" prefetch={true}>商品 123</Link>
// 使用 fetch + revalidate
const res = await fetch('https://api.example.com/product/123', { next: { revalidate: 60 } });
const product = await res.json();
Next.js 客户端补丁(SWR)
'use client';
import useSWR from 'swr';
export default function Price({ id }: { id: string }) {
const { data } = useSWR(`/api/price/${id}`, fetcher, { revalidateOnFocus: false });
return <div>{data ? data.price : '加载中...'}</div>;
}
Nuxt 3:useAsyncData + ClientOnly
<script setup>
const { data: product } = await useAsyncData('product', () => $fetch(`/api/product/${id}`))
</script>
<template>
<ProductMain :product="product" />
<ClientOnly><StockAndPrice :id="id" /></ClientOnly>
</template>
最终结论
- SSR 的卡顿是机制性问题:只要渲染发生在服务端,就存在网络与 server 渲染的延迟,hydrate 仍需客户端工作。
- Prefetch 是最直接的缓解手段,能显著降低感知延迟,但不能解决 server 端必须实时计算的场景。
- 最终策略往往是混合的:SSG/ISR 保证 SEO 与首屏性能,CSR / Client Components 提供动态与交互流畅度,Prefetch + SWR/React Query + Edge 缓存把体验调到最佳平衡点。