资讯详情

资讯详情

一文搞懂CSR、SSR、SSG:前端渲染原理、选型与工程落地

一次技术评审会上团队又为新项目该用哪种渲染方案争论起来。支持纯客户端渲染的同学说部署简单、交互爽坚持服务端渲染的说SEO不能丢还有人直接拍板要搞静态站理由是性能最好。争了半小时发现大家连一个问题都没对齐CSR、SSR、SSG这三种方式到底是在哪一步把页面变成用户能看到的HTML这篇东西想做的就是把这三条路彻底讲透。从底层原理、落地代码到选型逻辑再到我在真实项目里踩过的各种坑一次性说清楚。适合刚接触前端工程化、想搞明白渲染原理的同学也适合自己做博客、文档站、电商站点、为选型头疼的开发者以及需要向团队解释技术方案的负责人。1. 三种渲染方式到底是什么先搞懂HTML在哪一步被生成浏览器拿到网页之后做的事情其实很简单下载HTML解析成DOM树加载CSS和JS最后把页面画出来。所以“渲染方式”本质上是在回答同一个问题——页面内容是什么时候变成HTML的这决定了首屏速度、SEO表现、服务器开销和开发复杂度。1.1 CSR浏览器先拿空壳再靠JavaScript现拼CSR全称Client-Side Rendering客户端渲染。它的流程是这样的用户访问域名服务器返回一个几乎只有div idroot/div的空HTML文件里面带上一堆JS脚本的引用。浏览器下载并执行这些JSJS再去调接口拿数据最后把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渲染发生在何处构建机器、服务器、浏览器维度CSRSSRSSGHTML生成时机浏览器执行JS后服务端收到请求后项目构建时首屏性能慢依赖JS执行和接口中受服务端数据接口影响最优CDN直出SEO不友好友好友好部署方式静态托管Node/服务端运行静态托管CDN内容更新实时实时需重新构建服务器成本最低最高最低适用场景后台系统、重交互应用内容型站点、强个性化博客、文档、官网理解了这三个维度的差异再看各种框架的特性就不会被绕晕了。2. 代码层面落地三种方式分别怎么实现理论说了一堆不动手等于白说。下面分别用最朴素的方式把三种渲染流程实现一遍用的是原生技术栈。这样做是为了让你看清底层逻辑而不是被某个框架的API包装住。2.1 手写一个最简CSR页面CSR的核心就是“空HTML JS渲染”。我建一个最简项目目录结构如下csr-demo/ ├── index.html ├── main.js └── package.jsonindex.html里面只有两行关键内容!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleCSR演示/title /head body div idroot/div script typemodule src/main.js/script /body /htmlmain.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 classcard 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 classcard h2${item.title}/h2 p${item.desc}/p /div) .join(); return !DOCTYPE html html langzh-CN head meta charsetUTF-8 titleSSR演示/title stylebody { font-family: sans-serif; } .card { padding: 16px; border-bottom: 1px solid #eee; }/style /head body div idroot ${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; charsetutf-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 langzh-CN head meta charsetUTF-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上用户访问/aboutCDN直接返回对应的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然后拿预算去卡方案。如果需求允许SSGSSG几乎总能达标如果必须实时数据判断是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。现在我选型前都会先问两个问题这个页面是给谁看的内容多久变一次。再拿性能预算去卡一下方案不神化任何一种渲染方式也不排斥任何一种该混合就老老实实做混合。前端渲染没有银弹只有净在一起的工程权衡。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →