浏览器拿到的第一份 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 descriptioncanonical
看得见的
<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.pushStatesitemap 提交真实可访问的 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 里,有没有他们要的那句话?
有,渲染模型就不是瓶颈。没有,换框架也救不了。