Birditch的博客 Birditch的博客

页面到底是谁画的:CSR、SSR 和 SEO 那些容易混的账

浏览器拿到的第一份 HTML 里有没有字,决定了用户等多久、搜索引擎能不能读到你。CSR 和 SSR 吵了好几年,真正要分清的其实就这一件事。

第一次把一个营销站从 Vue SPA 迁到 Next.js 的时候,产品经理丢过来一句:「为什么上线三个月了,百度还是搜不到我们?」

我当时下意识想甩锅:收录就是慢。后来打开「查看网页源代码」,首页几乎是空的——<div id="app"></div>,字全在 JS 里。Google 现在多半还能把 JS 跑一遍;百度、很多社交爬虫、一部分做引用的机器人,对着空壳就是没辙。

CSR 和 SSR 被讲成了两种信仰。拆开看,差别没那么玄:

第一份送到浏览器(以及爬虫)手里的 HTML,里面有没有你想被看见的内容。

下面把 CSR、SSR,以及旁边那几个经常一起出现的词——SSG、ISR、Hydration——分开说。会夹一点 Next.js 的例子,因为现在大部分人是在那儿撞上这堆缩写的。SEO 相关的坑放在后面单独讲,免得跟渲染模型搅成一锅。


一、先把几个缩写摊开

1. CSR:Client-Side Rendering

客户端渲染。服务器给你一个壳,JavaScript 在浏览器里拉数据、画页面。create-react-app、Vite + Vue、早期的纯 SPA,走的都是这条路。

2. SSR:Server-Side Rendering

服务端渲染。请求来了,服务器先把当前该看的 HTML 拼好再吐出去。浏览器拿到的时候,字已经在上面了。Next.js 的 Server Component、Nuxt、以及你现在看的这个 Halo 博客,都属于「HTML 在响应里」这一挂。

3. SSG 和 ISR

SSG 是 Static Site Generation,静态生成:HTML 在构建的时候就做好了,之后每个请求直接端文件。博客、文档、官网特别合适。

ISR 是 Incremental Static Regeneration,增量静态再生成:先给缓存着的那份静态页,过期了在后台再生成一份新的。不用每次请求都算一遍,也不用整站重新构建。

4. Hydration:水合

SSR / SSG 把 HTML 送过来之后,React、Vue 还得在浏览器里把这坨静态 DOM「激活」成能点、能改状态的组件树。这一步叫 hydration。它有成本,也是 SSR 应用「看起来出来了,点了没反应」的常见原因。

请求走一圈,大概是这样:

CSR
  浏览器 --GET /--> 服务器 --> 空壳 HTML + JS 包
  浏览器 --再请求 API--> 服务器 --> JSON
  浏览器自己把 DOM 拼出来

SSR
  浏览器 --GET /--> 服务器(查库、跑组件、拼 HTML)
  服务器 --> 带正文的 HTML
  浏览器再下载 JS,把页面 hydrate 成可交互的

图里没画 SSG,是因为它和 SSR 对浏览器来说长得一样:都是「第一份 HTML 里就有字」。差别只在这份 HTML 是请求来了才算,还是构建时就算好放在边上。


二、CSR:浏览器自己拼页面

1. 它到底在干什么

服务器吐出来的通常就这点:

<div id="root"></div>
<script src="/app.js"></script>

真正的内容要等 JS 跑起来:

async function render() {
  const res = await fetch("/api/posts");
  const posts = await res.json();
  document.getElementById("root")!.innerHTML = posts
    .map((p) => `<article><h2>${p.title}</h2></article>`)
    .join("");
}

render();

右键「显示网页源代码」,你看不到那些 <article>。它们是后来才被写进 DOM 的。

2. 为什么大家爱用

CSR 不是过时技术,它在该用的地方非常舒服:

  • 部署简单。构建完丢 CDN,后面全是静态文件,服务器几乎不用动。

  • 交互重的页面很合适。后台、编辑器、画板、聊天窗,用户进来就是干活的,不需要被 Google 编进索引。

  • 切页快。壳已经在了,后面只换数据,不用整页刷新。

管理后台我到现在还是偏 CSR。让服务器为每一个「已登录用户的表格筛选」都拼一份 HTML,性价比不高。

3. 它的代价

首屏你得等 JS 下载、解析、执行、再等接口。网差、手机一般的时候,白屏或者转圈会很明显。

对 SEO 更麻烦。搜索引擎先拿到的是空壳。Google 后来加了渲染这一步,能执行 JavaScript,但那是「能」,不是「跟看普通 HTML 一样稳」。

Google 处理带 JS 的页面大致分三步:爬取 → 渲染 → 索引。爬取看的是原始 HTML;渲染要进 WRS(Web Rendering Service)排队。两波之间可能隔几个小时,页面一多也可能隔更久。渲染还吃 crawl budget——你站点页面越多,这个排队越明显。

百度对 JS 一直更不友好。微信、微博、Telegram 的链接预览基本不跑你的 React。所以那句「Google 会跑 JavaScript,CSR 做 SEO 没问题」——对 Google 都不完全对,对中文搜索更是想当然。

CSR 不是不能做网站,是别拿它硬做需要被搜索引擎和分享卡片看见的页面


三、SSR:服务器先把 HTML 吐出来

1. 请求来了再画

SSR 的意思很朴素:用户(或爬虫)来要这个 URL,服务器把这个 URL 对应的 HTML 算出来,再返回。

Next.js App Router 默认就是 Server Component,文件顶上不写 'use client' 的话,这个组件在服务器上跑:

// app/products/[slug]/page.tsx
// 没有 'use client',这页在服务器上执行
export default async function ProductPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const product = await db.product.findUnique({ where: { slug } });

  if (!product) {
    notFound(); // 老老实实 404,别返回 200 再在客户端说找不到
  }

  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </article>
  );
}

爬虫拿到的响应里,<h1> 和正文已经在了。不需要再等 useEffect 去拉接口。

Halo 这套博客也是同一类思路,只是实现换成了 Thymeleaf:文章存在数据库里,请求来了由服务器渲染成完整 HTML。你现在看的这页,右键「显示网页源代码」就能看见我正在写的这些字。这就是搜索引擎喜欢的形态。

2. 它解决什么

  • 第一份 HTML 里有内容。用户更快看见字,LCP(Largest Contentful Paint,最大内容绘制)通常更好。

  • 爬虫不用等 JS。Google 的第一波索引就能吃到正文;不跑 JS 的机器人也能读。

  • 密钥和拼装逻辑可以留在服务器。数据库连接、鉴权、把 Markdown 编成 HTML,都不必暴露到浏览器。

3. SSR 不是免费的

「SSR 一定更快」这话太满了。它快的是「第一次看见字」,不是「第一次能点」。

服务器每次都要查库、跑组件。TTFB(Time To First Byte,首字节时间)可能比静态文件差一截。你还得养一个 Node 进程,不像纯静态那么能扔在对象存储上不管。

更烦人的是 hydration。HTML 出来了,React 还要把事件绑上去。JS 包一大,主线程一忙,用户会碰到「按钮看得到,点了没反应」。体感上,一个算了 400ms 的 SSR 页面再 hydrate 1 秒,不一定比 CSR 加骨架屏爽。

还有缓存。购物车、登录态、推荐列表如果整页都动态算,CDN 基本帮不上忙。常见做法是:能静态的外框静态,动态的那一块单独流式填进去——这也是后来 Partial Prerendering(部分预渲染)想解决的事。


四、SSG 和 ISR:别把静态跟动态对立起来

很多人一听 SSR 就觉得「动态的、高级的」,一听静态就觉得「老土的、生成完就不能改的」。按现在的框架,这俩经常是同一套代码,只是这份 HTML 什么时候算不一样。

  • 构建时算好 → SSG

  • 请求来了再算 → SSR

  • 算好先用着,过期了后台再算一份 → ISR

Next.js App Router 里,给页面加一行就接近 ISR:

export const revalidate = 3600; // 秒。一小时内都端同一份,过期后下一次访问触发重新生成

什么时候用哪一种,我自己的经验很土:

  • SSG:博客、文档、价格页、changelog。内容改了就重新构建,完全够。

  • ISR:商品列表、运营会改的活动页。想被搜到,又不想每次请求都打数据库。

  • SSR:结果跟「此刻这个用户、这笔库存」强相关,缓存容易出事的那些页。

Next.js 这几年还在推 PPR:静态壳先出去,动态洞(购物车、推荐)再流式填。知道有这回事就行。没必要每个项目一上来就上实验特性——把 CSR / SSR / SSG 用对,已经能解决 90% 的问题。


五、SEO 真正在乎什么

很多人把 SEO 约等于「上 SSR」。太糙了。

搜索引擎要的是:它请求这个 URL 的时候,能便宜、稳定地读到标题、正文、链接、状态码。SSR 只是达到这个目标最省心的手段之一,不是目标本身。

1. 第一份 HTML 里要有该有的东西

这些最好在响应里就存在,而不是 useEffect 之后才写进 document.title

  • <title> 和 meta description

  • canonical

  • 看得见的 <h1> 和正文

  • Open Graph / Twitter Card——微信、Slack、iMessage 的链接预览看的就是它们,不看你 hydrate 之后的 DOM

  • JSON-LD 结构化数据

Next.js 里用 generateMetadata,不要在客户端组件里靠钩子改标题:

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);
  return {
    title: post.title,
    description: post.excerpt,
    alternates: { canonical: `https://birditch.com/archives/${slug}` },
  };
}

2. 状态码要诚实

CSR 路由里一个很常见的坑:/blog/does-not-exist 返回 200,JS 跑完再在页面上写「没有这篇文章」。对搜索引擎来说,这是一个空内容的正常页面,不是 404。

该 404 就 404,该 301 就 301。SSR / SSG 天然好做这件事,因为状态码是服务器回的。CSR 要处理的话,得在边缘或者源站就拦住,不能全交给前端路由。

3. 「Google 能跑 JS」≠「CSR 对 SEO 友好」

Google 官方自己也写过:server-side 或预渲染仍然是好主意。原因很直白——对用户和爬虫都更快,而且不是所有机器人都会跑 JavaScript

就算只看 Google:

  • 渲染要排队,新页面进索引会慢一截

  • WRS 不是完整 Chrome,有的脚本、有的 API 它不跑

  • 超时、错误、被机器人检测拦住的请求,都会让正文干脆不被索引

做中文站就更别赌百度。要收录,给完整 HTML。

4. 性能本身是排名因素

Core Web Vitals 还在:LCP、INP(Interaction to Next Paint,交互到下一次绘制)、CLS(Cumulative Layout Shift,累计布局偏移)。

SSR 救不了打包失控。2MB 的 JS 照样砸给浏览器,hydrate 把主线程打满,INP 照样烂。渲染模型决定「字什么时候出现」,包体和主线程决定「点了什么时候有反应」。两件事都要管。

5. 内链、sitemap、结构化数据

这些跟 CSR / SSR 正交,但经常被一起忘掉:

  • 重要页面要有真的 <a href>,不要全靠点击之后才 history.pushState

  • sitemap 提交真实可访问的 URL

  • JSON-LD 放在 SSR 的 HTML 里才稳。CSR 里用 useEffect 注入,Google 也许看得到,别的引擎不一定


六、怎么选,别站队

我自己做选择的时候,几乎只问两个问题:

这份内容需不需要被搜索引擎、社交卡片、AI 引用看到?

它跟「此刻这个用户」绑定有多紧?

粗分可以看这张表:

场景更合适的原因
管理后台、编辑器、登录后的工具CSR不需要被索引,交互重,部署也省事
博客、文档、官网、产品介绍SSG 或 SSR要被搜到,内容相对稳
商品详情、用户生成内容SSR 或 ISR既要新鲜,也要被搜到
同一站点里的不同页面混用按路由选策略,这才是现在的默认答案

Next.js、Nuxt 这类框架存在的意义,本来就不是逼你站队,而是允许一个项目里营销页走 SSG、商品页走 SSR、控制台走 CSR。App Router 把 Server Component 当默认,'use client' 是边界,不是装饰。

我自己踩过的坑,收成三句就够:

  • 需要被看见的页面,别把正文只放在客户端请求里。

  • 不需要被看见的页面,上 SSR 是给服务器加戏。

  • 现在几乎没有「整个站点纯 CSR 或纯 SSR」的好理由。

下次再听到「我们是 CSR 所以 SEO 不行」,或者「上了 Next.js SEO 就好了」,可以先问一句:用户和爬虫拿到的第一份 HTML 里,有没有他们要的那句话?

有,渲染模型就不是瓶颈。没有,换框架也救不了。