作为开发人员,我们经常面临会影响应用程序整个架构的决策。 Web 开发人员必须做出的核心决策之一是在他们的应用程序中实现逻辑和呈现的位置。 这可能很困难,因为有许多不同的方法来构建网站。
我们对这一领域的理解源于我们过去几年在 Chrome 中与大型网站的交流工作。 从广义上讲,我们鼓励开发人员考虑服务器渲染或静态渲染,而不是完全重新 hydration 的方法。
为了更好地理解我们在做出这个决定时选择的架构,我们需要对每种方法和在谈论它们时使用的一致术语有深刻的理解。 这些方法之间的差异有助于从性能的角度说明在 Web 上渲染的权衡。
渲染
- SSR:服务器端渲染 - 在服务器上将客户端或通用应用程序渲染为 HTML。
- CSR:客户端渲染 - 在浏览器中渲染应用程序,通常使用 DOM。
- Rehydration:在客户端“启动”JavaScript 视图,以便它们重用服务器渲染的 HTML 的 DOM 树和数据。
- Prerendering:在构建时运行客户端应用程序以将其初始状态捕获为静态 HTML。
- TTFB:Time to First Byte - 被视为单击链接和第一个内容进入之间的时间。
- FP:First Paint - 任何像素第一次对用户可见。
- FCP:First Contentful Paint - 请求的内容(文章正文等)变得可见的时间。
- TTI: Time To Interactive - 页面变为可交互的时间(事件连接等)。
Server Rendering
服务器渲染为服务器上的页面生成完整的 HTML 以响应导航。 这避免了在客户端上进行数据获取和模板化的额外往返,因为它是在浏览器获得响应之前处理的。
服务器渲染通常会产生快速的首次绘制 (FP) 和首次内容绘制 (FCP)。 在服务器上运行页面逻辑和渲染可以避免向客户端发送大量 JavaScript,这有助于实现快速交互时间 (TTI)。 这是有道理的,因为通过服务器渲染,您实际上只是向用户的浏览器发送文本和链接。 这种方法可以很好地适用于各种设备和网络条件,并开启了有趣的浏览器优化,例如流式文档解析。
通过服务器渲染,用户不太可能在使用您的网站之前等待 CPU 绑定的 JavaScript 处理。即使无法避免第三方 JS,使用服务器渲染来降低您自己的第一方 JS 成本也可以为您提供更多的“预算”。但是,这种方法有一个主要缺点:在服务器上生成页面需要时间,这通常会导致首字节时间 (TTFB) 变慢。
服务器渲染是否足以满足您的应用程序在很大程度上取决于您正在构建的体验类型。关于服务器渲染与客户端渲染的正确应用程序存在长期争论,但重要的是要记住,您可以选择对某些页面使用服务器渲染而不是其他页面。一些网站已经成功地采用了混合渲染技术。 Netflix 服务器呈现其相对静态的登陆页面,同时为交互密集型页面预取 JS,为这些较重的客户端呈现页面提供更好的快速加载机会。
许多现代框架、库和架构使得在客户端和服务器上呈现相同的应用程序成为可能。这些技术可用于服务器渲染,但重要的是要注意渲染发生在服务器和客户端的架构是它们自己的解决方案类,具有非常不同的性能特征和权衡。 React 用户可以使用 renderToString() 或基于它构建的解决方案,例如 Next.js 进行服务器渲染。 Vue 用户可以查看 Vue 的服务器渲染指南或 Nuxt。 Angular 有 Universal framework.大多数流行的解决方案都采用某种形式的 hydration 作用,因此在选择工具之前请注意使用的方法。
Static Rendering
静态渲染发生在构建时,并提供快速的首次绘制、首次内容绘制和交互时间 - 假设客户端 JS 的数量有限。 与服务器渲染不同,它还设法实现始终如一的快速首字节时间,因为页面的 HTML 不必即时生成。 通常,静态呈现意味着提前为每个 URL 生成单独的 HTML 文件。 通过提前生成 HTML 响应,可以将静态呈现器部署到多个 CDN 以利用边缘缓存。
静态渲染的解决方案有各种形状和大小。 Gatsby 之类的工具旨在让开发人员感觉他们的应用程序是动态呈现的,而不是作为构建步骤生成的。其他像 Jekyll 和 Metalsmith 这样的公司拥抱它们的静态特性,提供了一种更加模板驱动的方法。
静态呈现的缺点之一是必须为每个可能的 URL 生成单独的 HTML 文件。如果您无法提前预测这些 URL 的内容,或者对于具有大量独特页面的网站,这可能具有挑战性甚至不可行。
React 用户可能熟悉 Gatsby、Next.js 静态导出或 Navi - 所有这些都可以方便地使用组件进行创作。但是,了解静态渲染和预渲染之间的区别很重要:静态渲染页面是交互式的,无需执行大量客户端 JS,而预渲染改进了必须启动的单页应用程序的首次绘制或首次内容绘制客户端以使页面真正具有交互性。
如果您不确定给定的解决方案是静态渲染还是预渲染,请尝试以下测试:禁用 JavaScript 并加载创建的网页。对于静态呈现的页面,大多数功能在没有启用 JavaScript 的情况下仍然存在。对于预渲染页面,可能仍然有一些基本功能,如链接,但大部分页面将是惰性的。
另一个有用的测试是使用 Chrome DevTools 降低网络速度,并观察在页面变为可交互之前已下载了多少 JavaScript。预渲染通常需要更多的 JavaScript 才能获得交互性,而且 JavaScript 往往比静态渲染使用的渐进增强方法更复杂。
Server Rendering vs Static Rendering
服务器渲染不是灵丹妙药——它的动态特性会带来大量的计算开销成本。许多服务器渲染解决方案不会提前刷新,可能会延迟 TTFB 或将发送的数据加倍(例如,客户端上的 JS 使用的内联状态)。在 React 中,renderToString() 可能很慢,因为它是同步的和单线程的。使服务器渲染“正确”可能涉及为组件缓存找到或构建解决方案、管理内存消耗、应用记忆技术以及许多其他问题。您通常会多次处理/重建同一个应用程序 - 一次在客户端上,一次在服务器中。仅仅因为服务器渲染可以使某些东西更快地显示出来并不突然意味着您要做的工作更少。
SSR 为每个 URL 按需生成 HTML,但可能比仅提供静态呈现的内容慢。如果您可以进行额外的工作,服务器渲染 + HTML 缓存可以大大减少服务器渲染时间。与静态渲染相比,服务器渲染的优势在于能够提取更多“实时”数据并响应更完整的请求集。需要个性化的页面是不能很好地与静态呈现一起工作的请求类型的具体示例。
在构建 PWA 时,服务器渲染也可以提供有趣的决策。使用全页面 Service Worker 缓存还是仅服务器渲染单个内容更好?
Client-Side Rendering (CSR)
客户端渲染 (CSR) 是指使用 JavaScript 直接在浏览器中渲染页面。 所有逻辑、数据获取、模板和路由都在客户端而不是服务器上处理。
对于移动设备,客户端渲染可能难以获得并保持快速。 如果做最少的工作,保持紧张的 JavaScript 预算并以尽可能少的 RTT 交付价值,它可以接近纯服务器渲染的性能。 使用 HTTP/2 服务器推送或 \ 可以更快地交付关键脚本和数据,这可以让解析器更快地为您工作。 像 PRPL 这样的模式值得评估,以确保初始和后续导航感觉即时。
客户端渲染的主要缺点是所需的 JavaScript 量会随着应用程序的增长而增长。 添加新的 JavaScript 库、polyfill 和第三方代码后,这变得尤其困难,它们会争夺处理能力,并且通常必须在呈现页面内容之前进行处理。 使用依赖于大型 JavaScript 包的 CSR 构建的体验应该考虑积极的代码拆分,并确保延迟加载 JavaScript——“只在需要时提供您需要的服务”。 对于交互性很少或没有交互性的体验,服务器渲染可以代表这些问题的更具可扩展性的解决方案。
对于构建单页应用程序的人来说,识别大多数页面共享的用户界面的核心部分意味着您可以应用应用程序外壳缓存技术。 与服务工作者相结合,这可以显着提高重复访问时的感知性能。
Combining server rendering and CSR via rehydration
通常被称为 Universal 渲染或简称为“SSR”,这种方法试图通过同时进行客户端渲染和服务器渲染之间的权衡来平衡。诸如整页加载或重新加载之类的导航请求由将应用程序呈现为 HTML 的服务器处理,然后用于呈现的 JavaScript 和数据被嵌入到生成的文档中。如果仔细实施,这将实现快速的第一次内容绘制,就像服务器渲染一样,然后通过使用称为(重新)Hydration 的技术在客户端再次渲染来“拾取 Pick up”。这是一个新颖的解决方案,但它可能有一些相当大的性能缺陷。
带有再 hydration 的 SSR 的主要缺点是它会对交互时间产生显着的负面影响,即使它改进了 First Paint。 SSR 的页面通常看起来像是在加载和交互,但在执行客户端 JS 并附加事件处理程序之前,它们实际上无法响应输入。在移动设备上这可能需要几秒钟甚至几分钟的时间。
也许您自己也经历过这种情况 - 在看起来页面已加载后的一段时间内,单击或点击什么也没做。这很快变得令人沮丧......“为什么什么都没有发生?为什么我不能滚动?”
A Rehydration Problem: One App for the Price of Two
Rehydration 问题通常比 JS 导致的延迟交互更糟糕。 为了让客户端 JavaScript 能够准确地“接收”服务器中断的位置,而不必重新请求服务器用于呈现其 HTML 的所有数据,当前的 SSR 解决方案通常将来自 UI 的响应序列化 数据依赖项作为脚本标签写入文档。 生成的 HTML 文档包含高度重复:
如您所见,服务器响应导航请求返回应用程序 UI 的描述,但它也返回用于组成该 UI 的源数据,以及 UI 实现的完整副本,然后在客户端启动 . 只有在 bundle.js 完成加载和执行后,这个 UI 才会变成交互式。
从使用 SSR 再水化的真实网站收集的性能指标表明,应强烈建议不要使用它。 归根结底,原因归结为用户体验:最终很容易让用户陷入“恐怖谷”。
Streaming server rendering and Progressive Rehydration
在过去的几年中,服务器渲染有了许多发展。
流式服务器呈现允许您以块的形式发送 HTML,浏览器可以在接收到时逐步呈现这些 HTML。这可以提供快速的首次绘制和首次内容绘制,因为标记更快地到达用户手中。在 React 中,流在 renderToNodeStream() 中是异步的——与同步 renderToString 相比——意味着 backpressure 得到了很好的处理。
渐进式 rehydration 也值得关注,这是 React 一直在探索的东西。使用这种方法,服务器渲染的应用程序的各个部分会随着时间的推移而“启动”,而不是当前一次初始化整个应用程序的常用方法。这有助于减少使页面具有交互性所需的 JavaScript 量,因为可以推迟页面低优先级部分的客户端升级以防止阻塞主线程。它还可以帮助避免最常见的 SSR Rehydration 陷阱之一,其中服务器渲染的 DOM 树被破坏然后立即重建——通常是因为初始同步客户端渲染需要尚未准备好的数据,可能正在等待 Promise解析度。
SEO Considerations
在选择在 Web 上呈现的策略时,团队通常会考虑 SEO 的影响。 通常选择服务器渲染来提供爬虫可以轻松解释的“完整外观”体验。 爬虫可能理解 JavaScript,但在它们的呈现方式中通常有一些值得关注的局限性。 客户端渲染可以工作,但通常需要额外的测试和工作。 如果您的架构很大程度上由客户端 JavaScript 驱动,那么最近动态渲染也成为一个值得考虑的选项。
如有疑问,Mobile Friendly 测试工具对于测试您选择的方法是否符合您的期望非常有用。 它显示了任何页面在 Google 抓取工具中的显示方式、找到的序列化 HTML 内容(在执行 JavaScript 之后)以及呈现期间遇到的任何错误的视觉预览。
总结
在决定渲染方法时,测量并了解您的瓶颈是什么。 考虑静态渲染或服务器渲染是否可以让您完成 90% 的工作。 使用最少的 JS 来主要发布 HTML 以获得交互体验是完全可以的。 这是一个方便的信息图,显示了服务器 - 客户端范围:
更多Jerry的原创文章,尽在:"汪子熙":