☰
一文搞懂CSR、SSR、SSG:前端渲染原理、选型与工程落地
2026/10/15 6:20:37 网站建设 项目流程

一次技术评审会上,团队又为新项目该用哪种渲染方案争论起来。支持纯客户端渲染的同学说部署简单、交互爽;坚持服务端渲染的说SEO不能丢;还有人直接拍板要搞静态站,理由是性能最好。争了半小时,发现大家连一个问题都没对齐:CSR、SSR、SSG这三种方式,到底是在哪一步把页面变成用户能看到的HTML?

这篇东西想做的,就是把这三条路彻底讲透。从底层原理、落地代码到选型逻辑,再到我在真实项目里踩过的各种坑,一次性说清楚。适合刚接触前端工程化、想搞明白渲染原理的同学,也适合自己做博客、文档站、电商站点、为选型头疼的开发者,以及需要向团队解释技术方案的负责人。

1. 三种渲染方式到底是什么:先搞懂HTML在哪一步被生成

浏览器拿到网页之后做的事情其实很简单:下载HTML,解析成DOM树,加载CSS和JS,最后把页面画出来。所以“渲染方式”本质上是在回答同一个问题——页面内容是什么时候变成HTML的?这决定了首屏速度、SEO表现、服务器开销和开发复杂度。

1.1 CSR:浏览器先拿空壳,再靠JavaScript现拼

CSR全称Client-Side Rendering,客户端渲染。它的流程是这样的:用户访问域名,服务器返回一个几乎只有<div id="root"></div>的空HTML文件,里面带上一堆JS脚本的引用。浏览器下载并执行这些JS,JS再去调接口拿数据,最后把DOM节点一个一个创建出来,页面这才算真正有了内容。

这个流程最大的问题是“白屏”。用户打开页面的那一刻,看到的不是内容,是空白。要等到HTML解析完、JS下载完、JS执行完、接口返回完、框架渲染完,内容才会出现。在弱网环境或者接口慢的时候,这个时间可能超过三秒,用户早就关掉了。

CSR的优点也明显:部署极其简单,只要一个静态服务器就行,不需要Node进程;前后端完全分离,接口可以复用给App和别的端;交互体验好,页面路由切换不刷新,像个真正的本地应用。

可以用餐厅来类比。CSR就像你走进一家餐厅,服务员递给你一本只有目录的菜单,你点了菜,后厨才开始备菜炒菜。端上来要多久,取决于厨师的水平和当时有多少桌客人。

1.2 SSR:服务端把菜炒好再端上桌

SSR全称Server-Side Rendering,服务端渲染。核心变化是:渲染HTML这件事从浏览器挪到了服务器上。用户请求页面时,服务器收到请求,拉取接口数据,在服务端把组件渲染成完整的HTML字符串,再带着这段HTML响应给浏览器。浏览器直接就能显示出内容。

这个模式的优点非常直接。首先是首屏速度快,用户不用等JS执行,HTML里已经带着内容了;其次是SEO友好,爬虫看到的是一篇有内容的文档,而不是一个空壳;再就是对低端设备更友好,渲染压力被服务器扛掉了一部分。

代价是什么?服务器压力大。每一次请求都要在服务端做一次渲染,如果流量大又没做缓存,服务端很容易成为瓶颈。TTFB(首字节时间)也会变慢,因为服务器要先等数据源返回,才能拼出HTML。还有一个隐形复杂度——代码要“同构”,同一套组件既要在Node.js里跑,也要在浏览器里跑,很多浏览器专属对象不能直接用。

还是用餐厅类比。SSR像是后厨先把菜做好再端出来,你坐下没多久就能吃上。问题是客人一多,厨师锅不够用,出菜就慢。

1.3 SSG:菜提前批量做好,客人来了直接端

SSG全称Static Site Generation,静态站点生成。它和SSR生成的内容是同一类东西——完整HTML。区别只在于生成时机:SSR是请求时生成,而SSG是构建时生成。你在本地或CI上跑一条build命令,程序把所有页面一次性渲染成静态HTML文件,然后扔到CDN上。用户访问的时候,CDN边缘节点直接把文件吐回去,服务器都不需要参与。

这几乎是性能最好的方案。首屏内容秒开,TTFB极低,部署成本趋近于零,没有服务端渲染逻辑所以攻击面也小,稳定性非常高。

但它的短板很致命:内容更新麻烦。网站发一篇新文章,不是直接在后台点发布就行,要重新执行构建、重新部署。另外一个问题是构建时间,页面模板非常多的时候,全量构建可能要跑上几小时。强个性化场景也不适合,因为静态文件对所有用户生成的是同样的内容。

类比一下:SSG是预制菜。餐厅提前把大量菜品做好封存,客人来了直接端。方便是方便,但菜的口味已经被固化在“预制”那一刻了。

1.4 三者的核心区别:渲染发生的时间和位置

三种方式放在一起对比,关键变量就是两个:

  • 渲染发生在何时:构建时(SSG)、请求时(SSR)、浏览器运行时(CSR)
  • 渲染发生在何处:构建机器、服务器、浏览器
维度CSRSSRSSG
HTML生成时机浏览器执行JS后服务端收到请求后项目构建时
首屏性能慢,依赖JS执行和接口中,受服务端数据接口影响最优,CDN直出
SEO不友好友好友好
部署方式静态托管Node/服务端运行静态托管+CDN
内容更新实时实时需重新构建
服务器成本最低最高最低
适用场景后台系统、重交互应用内容型站点、强个性化博客、文档、官网

理解了这三个维度的差异,再看各种框架的特性就不会被绕晕了。

2. 代码层面落地:三种方式分别怎么实现

理论说了一堆,不动手等于白说。下面分别用最朴素的方式把三种渲染流程实现一遍,用的是原生技术栈。这样做是为了让你看清底层逻辑,而不是被某个框架的API包装住。

2.1 手写一个最简CSR页面

CSR的核心就是“空HTML + JS渲染”。我建一个最简项目,目录结构如下:

csr-demo/ ├── index.html ├── main.js └── package.json

index.html里面只有两行关键内容:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>CSR演示</title> </head> <body> <div id="root"></div> <script type="module" src="/main.js"></script> </body> </html>

main.js用原生DOM操作模拟渲染:

// main.js const root = document.getElementById('root'); // 模拟异步请求获取数据 setTimeout(() => { const data = [ { title: 'CSR的核心特点', desc: '页面内容由浏览器JS动态生成' }, { title: '首屏体验', desc: '用户要先等JS执行完才能看到内容' } ]; const html = data .map(item => `<div class="card"> <h2>${item.title}</h2> <p>${item.desc}</p> </div>`) .join(''); root.innerHTML = html; }, 2000);

这样一个页面,实际访问时会在白屏状态停留至少两秒,然后内容突然出现。你可能会觉得两秒还好,但真实项目里这个链路更长:HTML下载、JS文件解析执行、框架运行时代码加载、业务代码执行、接口请求、接口返回、组件渲染。每一步都是网络交互,都会拉长白屏时间。

用框架开发时,CSR就是像React的createRoot().render()、Vue的createApp().mount()这样,把整个应用挂载到root节点。原理和我上面的原生代码完全一样,只是框架把渲染过程封装得更细致了。

2.2 手写一个最简SSR服务

SSR需要在服务端渲染HTML。这里我用Node.js原生HTTP模块来写,尽量去掉框架层依赖,把原理暴露出来。

// server.js const http = require('http'); const fs = require('fs'); // 模拟从数据库/接口获取数据 function fetchData() { return new Promise(resolve => { setTimeout(() => { resolve([ { title: 'SSR的核心特点', desc: '页面内容在服务器端生成' }, { title: 'SEO友好', desc: '爬虫能直接抓到完整HTML' } ]); }, 300); }); } function renderHTML(data) { const content = data .map(item => `<div class="card"> <h2>${item.title}</h2> <p>${item.desc}</p> </div>`) .join(''); return `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>SSR演示</title> <style>body { font-family: sans-serif; } .card { padding: 16px; border-bottom: 1px solid #eee; }</style> </head> <body> <div id="root"> ${content} </div> <script src="/client.js"></script> </body> </html>`; } const server = http.createServer(async (req, res) => { if (req.url === '/') { const data = await fetchData(); const html = renderHTML(data); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(html); return; } if (req.url === '/client.js') { // 用于浏览器端水合的JS const script = ` // 这里可以写客户端交互逻辑 // 比如给按钮绑定事件、做路由跳转等 console.log('脚本加载,页面完成水合'); `; res.writeHead(200, { 'Content-Type': 'application/javascript' }); res.end(script); return; } res.writeHead(404); res.end('Not Found'); }); server.listen(3000, () => { console.log('SSR server running at http://localhost:3000'); });

启动这个服务后,curl访问页面,返回的HTML里直接带着卡片内容。浏览器拿到就渲染,不依赖JS。注意renderHTML是纯函数,输入数据、输出HTML,挺好理解。

但真实SSR要比这个复杂得多。前端框架在服务端渲染时,不能直接生成“事件绑定”,只能生成HTML字符串。用户看到的页面虽然内容正确,但点击按钮不会响应。浏览器还需要再加载一份JS,重新执行一遍框架逻辑,把事件处理器绑定上去。这个过程叫水合(Hydration),也就是“同构”的含义——服务端渲染HTML供展示,客户端再跑一遍JS来接管交互。

2.3 手写一个最简SSG构建脚本

SSG的核心是“构建时渲染”。我写一个Node脚本,在构建阶段读取数据,生成一批静态HTML文件。

// build.js const fs = require('fs'); const path = require('path'); // 模拟构建时从CMS/接口拉取内容 const pages = [ { url: '/', title: '首页', content: '这是首页的静态内容' }, { url: '/about', title: '关于我们', content: 'SSG生成的项目介绍页' }, { url: '/posts/frontend-rendering', title: '前端渲染方式对比', content: 'CSR、SSR、SSG的详细分析文章' } ]; const outputDir = path.join(__dirname, 'dist'); fs.rmSync(outputDir, { recursive: true, force: true }); fs.mkdirSync(outputDir, { recursive: true }); for (const page of pages) { const html = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>${page.title}</title> </head> <body> <h1>${page.title}</h1> <p>${page.content}</p> <a href="/">首页</a> <a href="/about">关于我们</a> </body> </html>`; // 把 /posts/frontend-rendering 映射为目录 + index.html const filePath = path.join(outputDir, page.url === '/' ? 'index.html' : page.url + '/index.html'); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, html); } console.log('构建完成,文件已输出到 dist 目录');

运行node build.js之后,dist目录里会出现三个静态HTML。把它们放到任意静态托管或CDN上,用户访问/about,CDN直接返回对应的HTML文件,连服务端计算都不需要。

这里要注意SSG生成的页面并不排斥交互。页面主体是静态HTML,但你可以像CSR那样在HTML里引入一段JS,给某个区域加水合逻辑,比如评论框、搜索框、购物车按钮。这种模式在业界被称为“静态 + 交互增强”或岛屿架构,主体静态保证首屏,局部交互保证体验。

2.4 现代框架如何把三者统一起来

现在的主流前端框架几乎都把CSR、SSR、SSG做进了同一个项目里,通过配置或约定来决定每个页面的渲染模式。比如在某个React框架里,你可以在页面组件上导出一个getStaticProps函数来声明静态生成,导出getServerSideProps来声明服务端渲染,什么都不导出就是纯客户端渲染。Vue生态的框架也有对应的配置项或约定。

这种模式的好处是“页面级混合渲染”。一个项目里,营销首页用SSG保证秒开,商品详情页用SSG加壳再配合客户端数据更新,用户中心用SSR,后台管理模块用CSR。三种渲染方式不再是互斥的选项,而是可以组合的积木。

但框架封装也带来了黑盒问题。很多人只在配置项里改个true/false,却不知道构建时和运行时分别发生了什么。一旦遇到水合不匹配、增量更新不生效、TTFB居高不下这类问题,不懂原理的人会完全无从下手。所以我一直建议,不管用不用框架,都要先把原生实现跑一遍。

3. 选型怎么做:什么项目该用哪种渲染方式

技术选型没有标准答案,但有一套比较成熟的判断逻辑。我一般从四个约束条件出发,逐个过滤,最后留下的方案通常就是最合适的。

3.1 先看四个关键约束条件

第一个是内容时效性。你的内容更新频率是秒级、分钟级,还是天级?如果是天级甚至周级,SSG非常合适,构建成本摊下来几乎可以忽略。如果内容来自实时数据库,用户提交后需要立刻反馈到页面上,SSG就比较别扭。比如一个爬虫展示平台,每分钟更新一批数据,全站SSG意味着每分钟跑一次全量构建,既不经济也不及时。

第二个是SEO需求。产品需不需要靠搜索引擎带来流量?如果你做的是官网、内容平台、电商站点,答案基本都是需要。搜索引擎爬虫虽然能执行JS,但效果不稳定,执行成本也高。对SEO有强需求的页面,SSR或SSG是正解。纯后台管理系统没有SEO诉求,CSR完全够用。

第三个是交互复杂度。像后台管理系统、数据可视化平台、代码编辑器这类重交互应用,页面主体是用户登录后才能操作的,SEO和首屏反而次要。这些场景用CSR能极大简化开发范式,没必要引入服务端渲染。

第四个是成本与运维。SSG只需要静态托管,扔到CDN上就行,几乎不要运维成本。SSR必须有Node服务,要做进程守护、日志、监控、横向扩容,运维复杂度直线上升。如果团队没有后端运维能力,或者预算有限,SSR要慎重。

结合这四个约束,我做了个汇总表:

场景内容时效性SEO需求推荐方案理由
企业官网低高SSG秒开+SEO好+成本低
博客/文档站低高SSG内容固定,构建一次用很久
新闻资讯中高SSR或SSG+增量既要内容相对新鲜,又要SEO
电商详情页中高SSG骨架SSR降级详情页内容变化不频繁,适合静态化
管理后台高低CSR交互复杂,服务端渲染收益低
用户个性化首页高中SSR或CSR+骨架屏内容因人而异,无法静态化
社交信息流高低CSR或SSR核心强交互,首屏需平衡

3.2 典型项目场景的对照分析

拿实际业务来举例。

内容型站点,比如技术博客或者文档平台,页面更新频率低,内容高度结构化,搜索引擎是重要的流量入口。这就是SSG的主场。用一个好一点的静态站点生成器,每次修改Markdown文件后执行构建,把产物推到CDN,性能和成本都无懈可击。

强交互应用,比如数据大屏、通过表单驱动的业务系统,用户必须先登录,爬虫根本进不来。CSR是最合理的选择。你没有必要为这类应用承担SSR的服务端运维和同构复杂度,做再多的SEO优化都是无用功。

电商场景是最典型的混合场景。商品详情页需要好的SEO索引,但商品信息更新不频繁,适合SSG先在构建时生成商品页HTML,价格库存等动态信息再在客户端异步刷新。购物车、结算、个人中心这类强交互模块,直接用CSR。首页和活动页根据运营频率决定:大促前需要频繁换页面,用SSR保证内容及时更新;日常营销页可以用SSG降低成本。

新闻门户和资讯平台则更依赖SSR或SSG加增量的方式。纯SSG的问题是内容发布后需要重新构建,跟不上热点;纯SSR在流量峰值时对服务端压力太大。很多团队的做法是SSG生成大部分常驻页面,配合上按需生成和CDN缓存,再对首页这类高流量页面做短时长的缓存,让性能保持在可接受范围内。

另一个常见误区是“SSR一定比CSR快”。不是的,关键在于数据链路。SSR把数据获取放在了请求链路里,如果上游数据库或接口本身需要2000ms才响应,那么TTFB就会被拖到2000ms以上,首屏照样慢。CSR虽然首屏看起来很慢,但如果做做了骨架屏和合理的接口预加载,用户体感未必差。渲染方式只是性能链路中的一环,不是银弹。

3.3 用性能指标来辅助决策

除了按业务需求拍板,还可以用性能预算来做数据支撑。常用的三个指标:

  • TTFB(首字节时间):从发起请求到收到响应第一个字节的时间
  • LCP(最大内容绘制):页面主要内容出现在屏幕上的时间
  • TTI(可交互时间):页面完整可交互的时间

用这三个指标套到渲染方式上,规律很明显。CSR的TTFB最快,因为服务器返回一个空壳HTML就完了,但LCP和TTI很慢,内容要靠后边的JS执行和接口。SSR的TTFB最不稳定,受服务端渲染时间和数据源延迟影响,但LCP非常快,HTML加载完内容就出来了。SSG的TTFB和LCP都是最优,静态文件直接CDN出,不用等服务。TTI方面,SSG和SSR都需要水合,水合完成后才能交互。

我建议在项目启动时定一个性能预算,比如TTFB小于800ms、LCP小于2.5s,然后拿预算去卡方案。如果需求允许SSG,SSG几乎总能达标;如果必须实时数据,判断是SSR还是CSR,就看看瓶颈在数据源还是JS执行——数据源是大瓶颈就倾向CSR,让HTML先出来、数据异步填充;JS执行是大瓶颈就倾向SSR,让服务端消化渲染压力。

4. 常见坑与我踩过的排查经验

三种渲染方式真实落地时,各自都会遇到一些“教科书里没写”的问题。我比较早做SSR项目的时候,被水合不匹配折磨过好几天;后来做内容站全面SSG化,又发现构建更新慢到被运营吐槽。这些经验值得单独写一节。

4.1 SSR的常见问题实录

水合不匹配是SSR最经典的坑。服务端生成HTML用的是服务端环境,客户端水合用是浏览器环境,两边DOM结构一旦对不上,React或Vue就会报警告,严重时会出现界面闪烁甚至事件绑定错乱。最常见的元凶就是Date.now()、Math.random()、读取window.innerWidth这类运行时值。我第一次遇到时,服务端和客户端渲染的时间戳不同,导致页面上有一块区域反复跳动,F12里明晃晃的水合警告,搜了半天才定位到是时间格式化组件在搞鬼。

解决思路很简单:这类依赖运行时的值不要直接参与服务端渲染。可以先用占位符,等客户端挂载后再填充真实值,或者统一时间基准,把同一个时间戳注入到服务端和客户端全局变量里。

SSR的另一个高频问题是内存泄漏。服务端是常驻进程,不像浏览器每开一个标签页就有一个新环境。我们的一个监控页面在服务端渲染时用了全局数组去缓存一部分历史数据,进程跑了两周,内存从200MB涨到3GB,最后被OOM杀掉。排查方法是先看堆内存快照,再看有没有事件监听器没释放,最后检查第三方客户端的长连接。建议进程管理配置自动重启,同时用LRU之类的策略限制缓存大小,比手工微调靠谱得多。

TTFB高的问题也经常出现。SSR把数据获取放到了请求链路里,如果依赖的上游接口某个时间段响应超过1秒,页面TTFB立刻飙升。我们的应对方式是给数据源加超时和降级:超过300ms还未返回就用缓存数据,缓存也没有就渲染一个简化版页面。页面主区域可以做缓存,Store、数据库这类数据源不怕慢,怕的是没有预案。

4.2 SSG的常见问题实录

SSG最常见的问题是内容过期。我做了一个小型的文档站,最初是全量构建,运营在后台改了一个错别字,前端页面在构建前一直是旧内容。后来接入了webhook,内容变更时自动触发增量构建,只重新生成了变更页面相关的文件,问题才算缓解。自动化这里的关键点是增量构建的实现,只有基础路径的页面重新生成,才能把更新时间控制在秒级。

动态路由也是SSG的一大难点。比如博客文章是/blog/[slug]这种形式,构建时你需要枚举所有slug,然后逐个生成页面。如果slug列表有几十万条,全量构建时间会非常恐怖。常规方案是开启按需生成或增量静态再生成——访问的路径如果还没生成静态页,先返回一个加载状态,同时触发构建,构建完成后再把页面缓存到CDN上。这么做解决了构建时长问题,但要注意首次访问的用户体验:他会先看到一个加载态,体验不如全量SSG。

构建时长本身也是个问题。某次客户有80万个页面模板,单网卡构建跑了接近3小时。后来做了构建优化,按页面模板类型拆成多个构建任务并行执行,才降到40分钟。如果页面量特别大,一开始就该评估全量构建的时间成本,别等到上线前才发现构建根本跑不完。

4.3 CSR的常见问题实录

CSR最大的问题是白屏,而且白屏的原因常常不止一种。排查顺序很重要,先打开DevTools看Network,确认HTML是否返回正常;再看脚本有没有加载成功,是不是CDN上的JS文件过期或跨域出问题;然后看Console有没有报错,接口是否返回了非2xx状态码;最后看是不是中间有JS执行异常导致挂载中断。我遇到过一个比较隐蔽的情况:页面在部分iOS旧版本设备上白屏,原因是某个JS库用了较新的语法特性,转译时漏掉了兼容配置。

路由懒加载失败也是CSR常见问题。动态import()写错路径,会导致某个路由在线上环境下加载404,刷新页面直接白屏。曾经排查过一个问题,本地跑得好好的,部署到线上后某路由刷新就挂,后来发现是分包后的文件名变化,加上CDN缓存了旧的index文件,导致引用关系断裂。解决办法是给动态路由配好分包命名策略,同时设置合理的CDN缓存时间,避免文件名指纹缓存不更新。

CSR的SEO补救方案通常有两个方向:一是用预渲染工具,在构建时把SPA的关键路由渲染成静态HTML,供爬虫读取;二是用爬虫代理,在服务端根据User-Agent判断是否是搜索引擎,是的话返回预渲染后的HTML给爬虫,是普通用户就正常走CSR。这两种方案作用域都有限,都不如直接上SSR或SSG背后,但如果团队已经选了CSR架构,也只能这样弥补。

4.4 一个可复制的渲染问题排查清单

不管用哪种渲染方式,问题排查都可以遵循一条主线:用户请求进来,HTML是在哪一步开始变的。我给自己整理了一份排查清单,按顺序执行,基本能覆盖大部分渲染问题:

首先用curl直接访问页面URL,看返回HTML里有没有真实内容。有内容,说明服务端或静态生成正常,问题可能在客户端水合或数据更新;没内容,说明走了CSR或者SSR的渲染环节没生效。

接着看浏览器Network面板里的主文档请求响应体,确认HTML内容完整,再看JS文件是否成功加载,有没有404、MIME类型错误或者跨域拦截。

然后看Console里的报错和水合警告。水合警告会让你不断向前排查,直到找出服务端和客户端为什么渲染不一致。

再看接口请求是否返回正常,客户端渲染的页面数据是否来自异步请求,数据挂起时页面是否显示loading态。

最后确认CDN缓存有没有策略问题,缓存时间过长会导致更新后的内容没有及时生效,缓存时间不合理又会频繁回源到服务端,增加压力。

这套流程我用过好几次,每次都能定位到具体环节。真正的问题往往不在技术方案本身,而在某个细节没有处理干净。

写到最后,分享几个我自己比较深的感受

三种渲染方式在真实项目里我都遇到过翻车。最早做内容站的时候,迷信SSG是性能最优解,结果运营天天抱怨发文章要重新构建,再加上一套自动化流程才把体验补到正常。后来做重交互应用,一开始选了SSR,结果同构代码的复杂度把小团队折腾得够呛,回调期快结束才想起来回归CSR。现在我选型前都会先问两个问题:这个页面是给谁看的,内容多久变一次。再拿性能预算去卡一下方案,不神化任何一种渲染方式,也不排斥任何一种,该混合就老老实实做混合。前端渲染没有银弹,只有净在一起的工程权衡。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询