为什么你的SSR会卡

51 阅读11分钟

前言

最近在看一些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 的“卡顿”来自三个叠加的环节

  1. 网络往返(RTT):即便内容固定,客户端仍需向 Server 请求新页面渲染结果(JSON/HTML),这有一段网络延迟。
  2. 服务端渲染时间:服务端需要执行 Server Component / Vue 组件的渲染逻辑(模板渲染、组件执行、可能的同步数据处理),即便数据很少,这也需要 CPU 时间。
  3. 客户端 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),并在客户端用 useFetchuseAsyncData 获取实时数据。

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

  1. 梳理页面按 SEO/实时性/个性化分组(先分类再决策)。
  2. 优先 SSG/ISR 对于 SEO 重的页面,确定 revalidate 策略。
  3. 对需要实时数据的字段使用 Client fetch(仅替换动态区域)。
  4. 开启路由 prefetch(检查 Link / NuxtLink 配置与行为)。
  5. 引入 SWR / React Query 做客户端缓存与 revalidate
  6. 对慢接口做 server side 缓存(Redis / edge),减少 server 渲染时间。
  7. 拆分大 bundle / 动态 import,减少 hydrate 成本。
  8. 实施 skeleton / Suspense 占位,优化感知速度
  9. 使用 RUM 监控路由切换时间,设立报警阈值
  10. 逐页 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 缓存把体验调到最佳平衡点。