Web渲染那些事儿

做为开发者,常常须要面对影响整个应用架构的决策。而Web开发者的核心决策之一,就是应用逻辑与渲染工做的实现,应处于架构中的什么位置(译注:客户端 or 服务器?)。如今有不少不一样构建网站的方法,所以这些决策变得越发困难。javascript

咱们对这一领域的理解,来自于咱们过去几年在 Chrome 工做中,与大型网站的交流。从广义上讲,咱们鼓励开发人员考虑经过一种称为 rehydration 的方式,进行服务器渲染或静态渲染。html

为了更好地理解在作出决定时所选择的架构,咱们须要对每种方法有充分的理解,而且在谈到它们时使用一致的术语。vue

术语

渲染java

  • SSR:服务器渲染(Server-Side Rendering)——在服务器上将客户端或通用(universal)应用程序渲染成HTML。
  • CSR:客户端渲染(Client-Side Rendering)——在浏览器中渲染App,一般使用DOM。
  • Rehydration:在客户端上“启动” JavaScript 视图,复用服务器渲染的HTML DOM树和数据。(译注:利用服务器返回HTML中的JS数据,从新渲染页面的技术,详见知乎讨论,其中《三体》的部分很形象~)
  • 预渲染(Prerendering):在构建时运行客户端应用程序,以将其初始状态捕获为静态HTML。

性能node

  • TTFB:首字节时间(Time to First Byte)——从点击连接 到 接收第一个字节内容 之间的时间。
  • FP:首次绘制(First Pain)——第一次有像素对用户可见的时间。
  • FCP:首次内容绘制(First Contentful Paint)——请求内容(文章正文等)变得可见的时间。
  • TTI:可交互时间(Time To Interactive)——页面变为可交互的时间(事件绑定等)。

服务器渲染(Server Rendering)

服务器渲染,指在服务器中生成整个页面的HTML,以此响应请求的技术。这样作避免了在客户端上进行数据获取的额外往返(round-trips)和模板处理,由于这些工做在浏览器得到响应以前,已由服务器处理了。react

服务器渲染一般会获得快速的首次绘制(FP)和首次内容绘制(FCP)。在服务器上运行页面逻辑和渲染,能够避免向客户端发送大量 JavaScript,有助于实现快速的可交互时间(TTI)。这之因此行得通,由于服务器渲染的本质,只是向用户浏览器发送文本和连接。这种方法适用于普遍的设备和网络,并能触发一些有趣的浏览器优化,好比流文档解析。git

图片描述

使用服务器渲染,用户再也不须要在客户端上等待 CPU 相关的 JavaScript 处理后,而后才能访问站点。即便第三方JS没法避免,使用服务器渲染来减小本身的JS成本,也能提供更多的性能“预算”。可是,这种方法有一个主要缺点:在服务器上生成页面有必定耗时,可能会致使较慢的首字节时间(TTFB)。github

服务器渲染是否知足应用程序,很大程度上取决于构建目标的体验类型。关于服务器渲染与客户端渲染的正确应用存在长期争论,但重要的是咱们能够选择对某些页面使用服务器渲染,而对其他页面不使用。一些网站已成功采用混合渲染技术:Netflix 服务器渲染其相对静态的落地页面,同时为交互繁重的页面预拉取JS,为这些重客户端页面提供更快的加载能力。web

许多现代框架、库和架构,使得在客户端和服务器上渲染相同的应用程序成为可能。这些技术可用于服务器渲染,可是要注意,在服务器和客户端上进行渲染的架构,都是各框架自家的解决方案,具备不一样的性能特色和权衡。React 用户可使用 renderToString() 或在其上构建的解决方案如 Next.js,用于服务器渲染;Vue 用户能够查看 Vue 的服务器渲染指南Nuxt;Angular 有 Universal。大部分流行的解决方案采用某种 hydration 的形态,所以在选择工具以前要注意使用的方法。shell

静态渲染(Static Rendering)

静态渲染在构建时进行,并提供快速的 FP、FCP 和 TTI——假设客户端JS的体积得当。与服务器渲染不一样,它还致力于实现始终如一的快速首字节时间(TTFB),由于页面的 HTML 没必要动态生成。一般,静态渲染意味着提早为每一个 URL 生成单独的 HTML 文件。经过预先生成 HTML 响应,能够将静态渲染部署到多个 CDN 以利用边缘缓存。(译注:也就是“页面静态化”)

图片描述

静态渲染的解决方案选择不少,像 Gatsby 这样的工具旨在让开发人员感受他们的应用程序是动态渲染的,而不是构建过程生成的。JekylMetalsmith 提供更多模板驱动的方法,更加符合它们的静态特质。

静态渲染的一个缺点是必须为每一个可能的 URL 生成单独的 HTML 文件。 若是没法提早预测这些 URL 的内容,或者对于具备大量不一样页面的网站,这可能具备挑战性甚至是不可行的。

React 用户可能熟悉 GatsbyNext.js 静态导出Navi ——它们均可以方便使用组件。可是,了解静态渲染和预渲染之间的区别很是重要:静态渲染页面是无需执行太多客户端 JS 就可交互的,预渲染则改进了单页面应用的 FP 或 FCP,因为是单页面应用,因此必须等待客户端启动过程,以使页面真正具备交互性。(译注:简单的说静态渲染不依赖客户端JS,适用于静态页面,而预渲染则依赖JS,更可能是为了富应用的初始界面加速)

若是不肯定选择静态渲染仍是预渲染方案,请尝试此测试:禁用JavaScript并加载建立的网页。对于静态渲染的页面,大多数功能在未启用JavaScript下仍然正常运做。而对于预渲染页面,一些基本功能(如连接)能正常展示,但页面其他部分没法正常展示。

另外一个有效的测试是使用 Chrome DevTools 减慢网络速度,并观察在页面变为可交互以前已下载了多少 JavaScript。预渲染一般须要更多的 JavaScript 来实现交互,而且这些 JS 每每比静态渲染使用的渐进加强方法更复杂。

服务器渲染 vs 静态渲染

服务器渲染并非银弹——它的动态特性带来显著的计算成本。许多服务器渲染解决方案会有耗时,致使延迟的 TTFB 或成倍的数据传输(例如,客户端 JS 所需的内联状态)。在 React 中,renderToString() 可能很慢,由于它是同步和单线程的。服务器渲染“正确”的姿式,可能涉及查找或构建组件缓存方案、内存消耗管理、应用记忆化技术以及许多其余方面。同一个应用程序一般须要屡次处理/重建——一次在客户端中,一次在服务器中。所以服务器渲染可使某些东西更快地显示出来,但并不意味着能够减小工做量。

服务器渲染为每一个 URL 按需生成 HTML,但速度可能比仅提供静态渲染内容要慢。若是加以进行额外的工做,服务器渲染 + HTML缓存,能够大大减小服务器渲染时间。服务器渲染的优点在于,可以提取更多“实时”数据,并响应比静态渲染更完整的请求集。个性化页面就是一个不适用于静态渲染的页面类型表明。

在构建 PWA 时,服务器渲染也抛出一个有趣的问题。 整个页面使用 Service Worker 缓存,与服务器渲染部份内容片断,哪一个方案更好?

客户端渲染(Client-Side Rendering,CSR)

客户端渲染(CSR)意味着使用 JavaScript 直接在浏览器中渲染页面。 全部逻辑、数据获取、模板和路由都在客户端处理,而不是服务器上。

客户端渲染很难在移动端作到很快。若是作好压缩工做,严格控制 JavaScript 预算,并在尽量少的 RTT 中提供内容,它能够接近纯服务器渲染的性能。使用 HTTP/2 Server Push 或 <link rel = preload> 能够更快地提供关键脚本和数据,这将使解析器更快地完成工做。像 PRPL 这样的模式值得评估,以确保初始和后续导航的即时感。

图片描述

客户端渲染的主要缺点是,随着应用程序的发展,所需的 JavaScript 数量会增长。随着添加新的 JavaScript 库、polyfill 和第三方代码,更是一发不可收拾。这些代码会竞争处理能力,而且一般必须在渲染页面内容以前完成处理。构建依赖大型 JavaScript 的 CSR 应用时,应该考虑积极的代码分割,并确保延迟加载 JavaScript——“只在须要时提供所需内容”。对于不多或没有交互性的页面,服务器渲染能够做为更具扩展性的解决方案。

对于构建单页应用程序的人来讲,识别大多数页面共享的UI核心部分,意味着能够应用 Application Shell 缓存技术。与 Service Worker 相结合,能够显著提升重复访问的感知性能。

经过 Rehydration 将服务器渲染和 CSR 相结合

这种方法一般被称为通用渲染或简称为“SSR”,它试图经过二者兼顾来平滑客户端渲染和服务器渲染之间的权衡。页面请求交由服务器处理,将应用程序渲染为 HTML,而后把用于渲染的 JavaScript 和数据,嵌入到生成的文档中。只要处理得当,这就像服务器渲染同样实现了快速的 FCP,而后经过称为 (re)hydration 的技术,在客户端上再次“拾取”来渲染。这是一种新颖的解决方案,但也具备一些明显性能缺陷。
译注:若是这里很差理解,请先理解上面术语部分中 Rehydration 的知乎连接内容。

rehydration 后的 SSR 主要缺点,是它会对可交互时间(TTI)产生显著的负面影响,即便它改善了首次绘制(FP)。SSR 页面一般看起来具备欺骗性的加载完成和可交互性,但在执行客户端JS并绑定事件处理以前,页面实际上没法响应输入。这在移动设备上可能持续几秒甚至几分钟。

也许你本身也经历过这种状况——在页面看起来已经加载后的一段时间内,点击或触摸什么都没反应。这很快变得使人沮丧......“为何没有反应? 为何我不能滚动?“

一个 Rehydration 问题:应用的双重成本

因为JS特性,Rehydration 问题每每比延迟交互更糟糕。为了使客户端 JavaScript 可以不用从新请求服务器,就能准确地获取服务器返回的用于呈现其 HTML 的全部数据,当前的 SSR 解决方案一般将UI的数据响应序列化, 以 Script 标签形式存放在 HTML 中。结果是生成的 HTML 文档包含大量重复片断:

图片描述

正如你所看到的,服务器除了返回应用程序 UI 以响应页面请求,还返回了用于组成该 UI 的源数据,以及生成相同 UI 的实现代码,即刻在客户端上运行。只有在 bundle.js 完成加载和执行后,页面才会变为可交互。

从使用 Rehydration SSR 站点收集的性能数据显示,这种用法应极力避免。归根结底,缘由归结为用户体验:很容易让用户处于“不明因此”的状态。

图片描述

Rehydration SSR 也不是没有但愿。在短时间内,仅将 SSR 用于高度可缓存的内容,能够减小 TTFB 延迟,从而达到与预渲染相似的结果。

流式服务器渲染和渐进式 Rehydration

服务器渲染在过去几年中发展迅猛。

流式服务器渲染能以 chunk 形式发送 HTML,浏览器能够在接收时逐块渲染。这促成了快速的 First Paint 和 First Contentful Paint,由于 HTML 标签更快地到达用户侧。在 React 中,流在 renderToNodeStream() 中异步处理,相比于同步的 renderToString,服务器的压力也会更小。

渐进式 Rehydration 也值得关注,React 一直在探索。使用这种方法,服务器渲染后的页面各部分,随着时间推移被“启动”,而不是一般一次初始化整个应用程序的作法。这能够减小页面可交互所需的 JavaScript 量,由于能够延迟页面低优先级部分,以防止阻塞主线程。它还能够帮助避免最多见的 SSR Rehydration 陷阱:服务器渲染的DOM树被破坏后当即重建——一般是由于客户端初始同步渲染所需的数据还没准备好,好比还在等待 Promise 的解析。

部分 Rehydration

部分 Rehydration 已被证实难以实现。该方法是渐进式 Rehydration 概念的扩展,经过分析渐进式 Rehydration 的各个部分(组件/视图/树),识别出那些不具交互性的部分。对于每一个基本静态的部分,相应的 JavaScript 代码会被转换为惰性引用和装饰功能,将其客户端占用空间减小到接近于零。部分 Rehydration 方案伴随着自身的问题和妥协。它为缓存带来了一些有趣的挑战,咱们没法假设服务器渲染的惰性部分 HTML,在页面完整加载前是可用的。

三方同构渲染(Trisomorphic Rendering)

若是可使用 service worker,“trisomorphic”渲染也颇有意思。该技术是指,利用流式服务器渲染初始页面,等 Service Worker 加载后,接管 HTML 的渲染工做。这可使缓存的组件和模板保持最新,并启用 SPA 式的导航以在同一会话中渲染新视图。当能够在服务器、客户端页面和 Service Worker 之间共享相同模板和路由代码时,此方法最有效。
图片描述

SEO 考虑

在选择渲染策略时,团队一般会考虑 SEO 的影响。为了让爬虫可以轻松得到“完整页面”,服务器渲染是不二的选择。虽然爬虫可能会理解 JavaScript,可是在渲染方式上的局限性须要注意。若是你的应用很是重 JavaScript,最近的动态渲染方案也是个值得考虑的选择。

若是有疑问,Mobile-Friendly Test 工具对于测试你选择的方法是否符合预期,很是有用。它展现了 Google 爬虫渲染页面的预览、序列化的 HTML 内容(执行 JavaScript 后),以及渲染过程当中发生的错误。
图片描述

总结

在决定渲染方式时,须要测量和理解真正的瓶颈在哪里。静态渲染或服务器渲染在多数状况都比较适用,尤为是可交互性对JS依赖较低的场景。下面是一张便捷的信息图,显示了服务器到客户端的技术频谱:
图片描述