你真的需要SSR吗?

952 阅读8分钟

当我们在谈论前端渲染方式的时候,更多是在围绕着这个页面,到底需要走客户端渲染(CSR)和还是走服务端渲染(SSR)。那么,这两种渲染方式的本质区别是什么?为什么网上有很多文章会默认SSR的渲染速度就比CSR的渲染速度快?以及,你真的清晰的知道当前项目到底需要哪一种渲染方式吗?

CSR

客户端渲染,是指页面并不是基于服务端下发的HTML文件来渲染页面,相反,客户端渲染所获取的Html文件,通常是一个只有根节点的空白页面,真正执行渲染行为的是挂载到这个Html文件里的JS脚本。当用户打开页面的时候,浏览器会先下载一个挂满JS脚本的Html文件,然后再去下载并执行这些JS脚本,最终通过JS脚本的代码去渲染出后续的页面元素,完成整个的渲染流程。
image.png

SSR

服务端渲染。在SSR中,服务端会根据用户请求的路径,在服务端一侧就将完整的Html文档拼接好,而当前的HTML文档是具有该页面真正的内容,但是他并不具备任何交互逻辑。同时,这个Html文档也包含了后续需要执行的JS脚本。
当用户拿到Html文件的时候,此时页面已经是具备可视元素的了(相比于CSR的白屏),当倘若用户需要和页面进行交互的时候(例如点击事件),还需要将文档的JS脚本进行下载和执行,将正确的事件处理方法结合到对应的Dom节点上,这个过程我们通常称之为hydration,也就是水合。
image.png

优劣势

1、渲染速度

对于CSR来说,在进行JS下载和解析的过程中,我们的页面始终是呈一个白屏状态的,如果JS的文件过大或者数量过多的话,用户侧的白屏时间会相当长,大大的降低用户的体验。而对于SSR来说,当用户下载完Html文件之后,页面就已经具备了完整的可视结构了,这对于白屏来说,显然是具备更大的优势的。所以针对于FCP来说,SSR的体验肯定是比CSR的要来的更加优秀一些。
但是,性能体验可不止FCP单个指标,我们还需要注意另外一个和用户体验息息相关的指标,TTI。
image.png

TTI 指标是测量页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间。
对于CSR来说,当页面渲染完成之后,便是立即可交互的。但对于SSR来说,当页面渲染完成之后,还需要等待一个hydration的过程,直到这个过程结束之后,页面才是真正可交互的。
所以,CSR和SSR的TTI时间大概可以总结为:

  • CSR = HTML下载的时间 + JS下载和执行的时间
  • SSR = 服务端拼接HTML的时间 + HTML下载的时间 + JS下载和执行的时间 + Hydration

总的来说,SSR的TTI通常来说会比CSR来的要长一些(但是并不绝对)。

2、用户交互体验。

当使用CSR的时候,只要页面初始化完成,后续用户所有的交互,包括交互的事件以及路由的跳转,都在同一个页面内进行,并由JS来接管,在页面变化的时候,利用Ajax等技术进行局部刷新,而不需要重新请求服务端生成新的Html文件。在这方面来说,对比SSR的多页面应用,显然是CSR更占优势。
但是,随着前端的发展,Next和Nuxt等SSR框架,也推出了首屏SSR,后续的页面走CSR的策略,大大的利用了双方的优势,从而给用户更好的使用体验。

3、数据请求的速度。

当前绝大多数的非静态页面,在初始化的时候,都需要跟服务端有数据的交互,利用服务端返回的数据,去动态的渲染页面的各个组件或者模块。而在CSR的渲染方式当中,通常会在页面元素加载之前,进行一个AJax请求,在得到返回的数据之后再进行后续的渲染。在SSR当中,这个流程也是类似的。
但是,我们需要注意到虽然流程是类似的,但发出这个数据请求的环境是截然不同的,对于SSR来说,服务端的网络环境要优于客户端的环境,内部服务器之间的请求通信路径也更短。对于CSR来说,用户的网络环境具有一定的不稳定性,且每次请求都得走完完整的HTTP/HTTPS的通讯流程,显然是会更慢的。

4、SEO

SSR对于SEO或者搜索引擎爬虫来说,显然是对CSR有压倒性的优势的。

无脑SSR?

综上以上四点的优劣势分析,好像每一样SSR都不占下风,并且在首屏渲染和SEO方面来说,更是占据了压倒性的优势,那这么说的话,是不是我们所有的页面都可以无脑使用SSR呢?
答案是否定的,上述的分析中,我们忽略了几个SSR存在的几个问题。

1、Hydration是完全多余的性能损耗。

在服务端接受到用户的请求之后,会根据路径创建出组件树,也就是静态的进行一次页面渲染,然后将渲染后的组件通过字符串拼接的方式返回到客户端。
客户端接到 SSR 响应之后,为了支持恢复页面的交互功能,仍然需要创建出组件树,与 SSR 渲染的 HTML 关联起来,并绑定相关的 DOM 事件,让页面变得可交互,这整个过程被称为 hydration。
hydration具体做了什么呢?他主要解决了两个问题:

  • What: 我们需要哪些事件处理程序?事件处理程序是一个包含事件行为的闭包。如果用户触发此事件,则应该发生这种情况。
  • Where: 我们需要将这些事件处理方法添加到哪个Dom节点?

而对于事件处理程序来说,通常内部都会对整个应用的状态进行处理,hydration的过程实际上也就是利用重新执行组件代码的方法,来让静态的HTML字符串恢复这个状态。
这里的状态实际上指的是两种:

  • APP_STATE:应用程序的状态。简单来说应用程序的状态就是 HTML 事件中的各个状态事件,如果不存在这些事件状态那么所有的内容都是没有任何交互效果的。
  • FRAMEWORK_STATE:框架内部状态。通常我们会利用诸如 React 或者 Vue 等框架进行接替渲染。如果没有 FRAMETER_STATE,框架内部就不知道应该更新哪些DOM节点,也不知道应该在什么时候更新它们。

所以,我们需要注意到,恢复HTML的交互性,或者说恢复HTML的状态实际上是一种纯粹开销的动作。
因为执行组件代码,生成组件树和对应的状态这个流程实际上在服务端已经走过一遍,只不过在下发到客户端的时候,这些信息被丢失了而已。如果服务器将组件树的状态和交互信息以序列化的形式,将其与 HTML 一起发送给客户端,就可以避免发生或者延迟这个hydration的过程,新框架Qwik实际上就是这么做的。
回到问题本身,hydration 所需加载、执行的 JavaScript 代码不见得比 CSR 模式少多少,这部分工作在客户端执行,受限于用户设备的性能,在较差的设备下可能会造成可感知的不可交互时间:

  • CSR:可交互但是没有数据(还在异步请求数据,可能会持续很长)
  • SSR:有数据但是不可交互(拉到 JS 后开始 hydrate 的过程,能看到内容但是不可交互,一般不会持续很长)

富交互的场景下,后者不一定比前者用户体验更好。

2、服务的稳定性和性能要求

与客户端程序相比,服务端程序对稳定性和性能的要求严苛得多,例如:

  • 稳定性:异常崩溃、死循环
  • 性能:内存/CPU 资源占用、响应速度(网络传输距离等都要考虑在内)

因此面临后端专业性问, 搭建一个高可用的 SSR 服务不是一件简单的事情,如何应对大流量/高并发,如何识别故障,如何降级/快速恢复,哪些环节需要加缓存,缓存如何更新……

最后

CSRSSR都是当前前端技术发展中的重要产物,两者之间并没有存在绝对的优劣势。
当面对内容密集型的页面时,首屏加载性能和可访问性相对来说非常重要,这时候SSR的应用优势会凸显出来。但如果面对交互密集型的页面来说,SSR能提前渲染的内容并不多,且hydration带来的设备压力也不小,这时候CSR可能更加明显。
当然,SSR结合CSR,SSG,以及ISR也是不同场景下一些非常不错的解决方案。