渲染模式
简介
当你开始构建一个新的 Web 应用程序时,你做出的基础决策之一是 - “我想要在哪里以及如何渲染内容?”。它应该在 Web 服务器、构建服务器、边缘网络或直接在客户端渲染吗?它应该一次性渲染、部分渲染还是逐步渲染?
这些关键决策的答案很大程度上取决于用例。“选择最合适的渲染模式可以对您为工程团队创建的开发体验 (DX) 和您为最终用户设计的用户体验 (UX) 产生巨大影响。
选择正确的模式可以带来更快的构建速度和出色的加载性能,同时降低处理成本。另一方面,错误的选择可能会扼杀本可以实现伟大商业理念的应用程序。因此,您必须确保您拥有的每个革命性想法都使用适当的渲染模式进行开发。
渲染模式的重要性
为了创建出色的 UX,我们通常会尝试针对以用户为中心的指标优化我们的应用程序,例如 核心 Web 指标 (CWV)。CWV 指标衡量与用户体验最相关的参数。优化 CWV 可以帮助确保出色的用户体验和我们的应用程序的最佳 SEO。
为了为我们的产品/工程团队创建出色的 DX,我们必须通过确保更快的构建时间、轻松回滚、可扩展的基础设施以及帮助开发人员取得成功的许多其他功能来优化我们的开发环境。
基于这些原则建立开发环境可以使我们的开发团队高效地构建出色的产品。
总结我们的期望,我们现在已经列出了相当长的清单。但是,如果您选择正确的渲染模式,您就可以直接获得这些好处中的大多数。
选择模式
渲染模式已经走过了很长一段路,从服务器端渲染 (SSR) 和客户端渲染 (CSR) 到今天在不同论坛上讨论和评判的高度细致的模式。虽然这可能会让人不知所措,但重要的是要记住,每个模式都是为了解决特定用例而设计的。对一个用例有利的模式特征在另一个用例中可能是弊端。同一个网站的不同类型的页面也可能需要不同的渲染模式。
Chrome 团队 鼓励 开发人员考虑使用静态或服务器端渲染而不是完全重新水合的方法。随着时间的推移,默认情况下,渐进式加载和渲染技术可以帮助在使用现代框架时在性能和功能交付之间取得良好的平衡。
接下来的章节将详细介绍不同模式 - 新旧模式。但首先,我们将简要介绍其中一些模式,以帮助您了解它们在哪些情况下最适合使用。
静态渲染
静态渲染是一种简单但功能强大的模式,您可以使用它来构建加载速度极快的网站,几乎可以实现即时页面加载。
使用静态渲染,整个页面的 HTML 在构建时生成,并且在下次构建之前不会更改。HTML 内容是静态的,并且可以轻松地在 CDN 或边缘网络上缓存。CDN 可以快速将预渲染的缓存 HTML 传递给客户端,当他们请求特定页面时。这大大缩短了在典型的服务器端渲染设置中处理请求、渲染 HTML 内容并响应请求所需的时间。
上述过程最适合那些很少更改并且无论谁请求它们都显示相同数据的页面。由于我们今天在网络上消耗了许多动态的、定制的数据,因此我们有了静态渲染的变体来满足不同的用例。
基本/纯静态渲染
由于静态渲染有很多变体,所以我们把之前讨论的主要技术称为纯静态渲染。您可以将它用于几乎没有动态内容的页面。
以下房地产网站演示中的页面始终向全球各地所有人显示相同的内容。它不包含任何动态数据或个性化内容。
当网站部署和构建(例如,在 Vercel 上)时,相应的 HTML 将生成并保存在服务器上的静态存储中。
当用户请求页面时,服务器会将预生成的 HTML 发送到客户端。此响应也会缓存到最靠近用户的边缘位置。然后,浏览器会渲染 HTML 并使用 JavaScript 捆绑包来水合页面。
纯静态渲染对于性能非常出色,因为它会导致非常快的 TTFB,因为 HTML 已经可以在服务器上获得。浏览器接收更快的响应并可以快速渲染它,从而导致快速的 FCP 和 LCP。由于内容是静态的,因此在渲染时不会发生布局偏移。
因此,纯静态渲染,尤其是使用 CDN 进行缓存,有助于实现出色的核心 Web 指标。但是,大多数网站至少有一些动态内容或用户交互。
带客户端 fetch
的静态渲染
假设我们想增强我们的房地产演示来显示最新的房源。我们必须使用数据提供者来获取这些房源。
在这种情况下,我们可以使用带客户端 fetch
的静态渲染。当您想要在每次请求时更新数据时,此模式非常有效。
您仍然可以使用静态渲染为网站渲染 UI,其中包含一个骨架组件,您希望在其中放置动态房源数据。然后,在页面加载后,我们可以在客户端(例如使用 SWR)获取数据。
一个自定义 API 路由用于从 CMS 获取数据并返回此数据。
当用户请求页面时,预生成的 HTML 文件将发送到客户端。用户最初会看到没有数据的骨架 UI。客户端从 API 路由获取数据,接收响应,并显示房源。(示例中不包括水合调用)接收响应并显示房源。(示例中不包括水合调用)
虽然带客户端 fetch
的静态渲染可以给我们带来良好的 TTFB 和 FCP,但 LCP 并不理想,因为“最大内容”只有在从 API 路由获取房源数据后才能显示。
也存在布局偏移的可能性,尤其是当骨架 UI 的大小与最终渲染的内容不匹配时。
另一个缺点是,这种方法会导致更高的服务器成本,因为我们每次页面请求都会调用 API 路由。
Next.js 提供了一些解决方案,如以下部分所述,以提高您的应用程序在处理动态数据时的性能。
带 getStaticProps
的静态
此方法允许您在构建时访问数据提供者并在服务器上获取数据。如果您知道静态页面上的动态数据始终可以在构建时获得,这可能是一个不错的解决方案。
getStaticProps
方法允许我们在服务器上使用数据生成 HTML。因此,我们可以避免创建 API 路由以在客户端获取数据。同样,在数据加载时不需要骨架组件,因为页面将使用数据渲染。
当我们构建项目时,数据提供者会被调用,返回的数据会被传递到生成的 HTML。
当用户请求页面时,该过程与纯静态渲染类似。响应会被缓存并渲染到屏幕上,浏览器会获取水合页面所需的 JavaScript 捆绑包。
从客户端的角度来看,网络和主线程的工作与纯静态渲染相同,因此我们获得了类似的出色性能。
随着网站的增长,使用此方法时的 DX 可能不会那么好。
对于静态构建数百个页面的网站(例如博客网站),反复调用 getStaticProps
方法会导致构建时间过长。如果您正在使用外部 API,您可能会遇到请求限制或产生高额的使用费用。
该方法也只适合我们能够以较低的频率在构建时更新数据的情况。频繁更新数据意味着我们必须经常重新构建和重新部署网站。
增量静态生成
我们可以使用增量静态生成来解决前面讨论的构建时间和动态数据问题。
ISR 是一种混合模式,因为它允许我们仅预渲染某些静态页面,并在用户请求时按需渲染动态页面。这会导致更短的构建时间,并允许在特定时间间隔后自动使缓存失效并重新生成页面。
假设我们现在想要显示单个房源的详细信息,以增强我们之前的演示。我们可以预渲染这些新页面,以便在用户点击房源时快速加载。
Next.js 使用 getStaticPaths 方法来生成动态路径,帮助我们实现这一目标。我们可以告诉 Next.js 根据它们的查询参数预生成哪些页面。
在我们的演示中,让我们获取所有列表并预先生成每个列表的页面。注意,如果列表数成千上万,这将需要很长时间。在这种情况下,我们必须告诉 Next 仅预先生成所有页面的子集,并在按需生成剩余列表页面时(当用户请求时)呈现一个回退。
预渲染和按需生成的页面以类似的方式交付。如果用户请求尚未生成的页面,它将按需生成并由 Edge 缓存。因此,只有第一个用户可能会对未预渲染的页面体验更差。其他人将从快速、缓存的响应中受益。
这解决了之前方法的长时间构建问题。但我们仍然有登陆页面,每次我们有新的列表时都需要重新部署。
为了能够刷新登陆页面,我们可以在特定间隔自动使缓存失效并在后台重新生成页面。我们可以通过向返回的对象添加 `revalidate` 字段来实现这一点。
如果用户请求的页面在缓存中停留的时间超过指定秒数,用户将最初看到过期的页面。页面再生将同时触发。一旦页面在后台重新生成,缓存就会失效并使用最近重新生成的页面更新。
使用增量静态再生,我们可以通过每隔几秒自动重新验证页面来显示动态内容。
虽然这已经比我们以前有了很大的改进,但也有一些缺点。我们的内容可能不像我们定义的间隔那样频繁地更新。这会导致不必要的页面再生和缓存失效。每次发生这种情况时,我们都会调用无服务器函数,这会导致更高的服务器成本。
按需增量静态再生
为了解决上面提到的最后一个缺点,我们有**按需增量静态再生**,它允许我们使用 ISR,但再生发生在特定事件而不是固定间隔。
我们不再使用 `revalidate` 字段,而是根据 API 路由中的新数据进行重新验证。
例如,我们可以监听一个传入的 webhook 事件,该事件告诉我们何时将新数据添加到我们的数据提供者。当我们调用 `revalidate` 方法时,指定路径上的页面将自动重新生成。
使用常规 ISR,更新后的页面仅在处理过用户页面请求的边缘节点上缓存。按需 ISR 会在整个边缘网络中重新生成和重新分发页面,以便全球用户可以从边缘缓存自动看到页面的最新版本,而不会看到过时的内容。我们还避免了不必要的再生和无服务器函数调用,与常规 ISR 相比,降低了运营成本。
因此,按需 ISR 为我们带来了性能优势和出色的 DX。
总的来说,静态生成是一个很棒的模式,它的变体,尤其是 ISR,可以涵盖各种用例。
它使我们能够以合理的成本拥有快速且动态的网站,这些网站始终在线。但是,在某些情况下,静态不是最佳选择,例如,对于每个用户都不同的高度动态、个性化页面。让我们看看哪些模式最适合这些情况。
服务器端渲染
使用服务器端渲染,我们为每个请求生成 HTML。这种方法最适合包含高度个性化数据的页面,例如基于用户 Cookie 的数据,或通常从用户的请求中获取的任何数据。它也适用于应阻止渲染的页面,可能是基于身份验证状态。
个性化仪表板是页面上高度动态内容的绝佳示例。大部分内容都基于用户的身份或授权级别,这些内容可能包含在用户 Cookie 中。此仪表板仅在用户经过身份验证时才会显示,并且可能显示特定于用户的敏感数据,这些数据不应对其他人可见。
Next.js 允许我们使用 `getServerSideProps` 方法在服务器上渲染页面。此方法在每次请求时都在服务器上运行,最终将返回的数据传递给页面以生成 HTML。
当用户请求页面时,`getServerSideProps` 方法运行,返回用于生成页面的数据,并将响应发送到客户端。然后,客户端渲染此 HTML,并可能发送另一个请求以获取用于使元素水化的 JavaScript 包。
生成的 HTML 内容对每个请求都是唯一的,不应由 CDN 缓存。
客户端的网络和主线程对于静态和服务器端渲染非常相似。FCP 几乎等于 LCP,我们可以轻松避免布局偏移,因为在初始页面加载后没有动态内容加载。
但是,服务器渲染页面的 TTFB 明显长于静态渲染,因为页面在每次请求时都从头开始生成。
虽然服务器渲染在您想渲染高度个性化数据时是一种很好的方法,但有一些事项需要考虑,以实现出色的用户体验并降低服务器成本。这些成本可能会很高,因为您会在每次请求时调用无服务器函数。
-
`getServerSideProps` 的执行时间
在 `getServerSideProps` 中的数据可用之前,页面生成不会开始。因此,我们必须确保 `getServerSideProps` 方法不会运行太长时间。
-
在与无服务器函数相同的区域部署数据库
如果数据来自数据库,我们必须减少查询数据库所需的时间。除了查询优化之外,您还应考虑数据库的位置。
如果您的无服务器函数部署在旧金山,但您的数据库在东京,建立连接并获取数据可能需要一段时间。相反,请考虑将数据库移至与无服务器函数相同的区域,以确保您的数据库查询更快地返回数据。
-
向响应添加 `Cache-control` 标头
改进 SSR 性能的另一个步骤是向响应添加 `Cache-Control` 标头。
-
升级服务器硬件
升级服务器硬件可以帮助快速处理单个请求并获得更快的响应。
Vercel 使用无服务器函数来服务器端渲染您的页面。
虽然无服务器函数有很多好处,例如只需为使用付费,但它也有一些限制。启动 lambda 所需的时间(称为长时间冷启动)是无服务器函数的常见问题。此外,与数据库的连接可能很慢。您也不应从地球的一侧调用位于另一侧的无服务器函数。
边缘 SSR + HTTP 流
Vercel 目前正在探索**边缘服务器端渲染**,这将使用户能够**从所有区域进行服务器端渲染**并体验**接近零的冷启动**。**边缘 SSR** 的另一个好处是边缘运行时允许**HTTP 流**。
使用无服务器函数,我们在服务器端生成整个页面,并在等待整个包在客户端加载和解析后才能开始水化。
使用边缘 SSR,我们可以在组件准备就绪后立即流式传输文档的各个部分,并以细粒度的方式水化这些组件。这减少了用户的等待时间,因为他们可以在组件一个接一个地流式传输时看到它们。
**流式传输 SSR** 还支持**React 服务器组件**。**将边缘 SSR 与 React 服务器组件结合使用**可以让我们拥有**静态和服务器渲染之间的完美混合**。
**React 服务器组件**允许我们在服务器上部分渲染 React 组件,这对那些需要大型依赖项但无需下载到客户端的组件很有用。
回到房地产网站的示例,如果我们想再次显示登陆页面并包含特定于区域的用户列表。页面的绝大部分只包含静态数据;只有需要基于请求的数据的列表。
我们现在可以选择只在服务器端渲染列表组件,而其他部分在客户端渲染,而不是必须服务器端渲染整个页面。虽然我们最初必须服务器端渲染整个页面才能实现这种行为,但现在我们可以获得**静态渲染的出色性能以及服务器端渲染的动态优势**。
结论
我们现在已经介绍了许多在服务器上渲染内容的模式。对于每个屏幕上的组件都可能根据用户交互而改变的非常动态的网站,仍然建议使用完全水化的客户端渲染(CSR)。
根据应用程序的类型或页面类型,某些模式可能比其他模式更适合。下图比较了不同模式的亮点,并为每种模式提供了用例。
以下来自 2022 年构建 JavaScript 网站的模式 的表格提供了一种以关键应用程序特征为中心的另一种视图。对于任何寻找适用于常见 应用程序全息类型 的模式的人来说,它应该有所帮助。
投资组合 | 内容 | 店面 | 社交网络 | 沉浸式 | |
---|---|---|---|---|---|
全息类型 | 个人博客 | CNN | 亚马逊 | 脸书 | Figma |
交互性 | 最小 | 链接文章 | 购买 | 多点、实时 | 一切 |
会话深度 | 浅 | 浅 | 浅 - 中 | 扩展 | 深 |
值 | 简单性 | 可发现性 | 加载性能 | 动态性 | 沉浸感 |
路由 | 服务器 | 服务器、混合 | 混合、过渡 | 过渡、客户端 | 客户端 |
渲染 | 静态 | 静态、SSR | 静态、SSR | SSR | CSR |
水化 | 无 | 渐进式、部分 | 部分、可恢复 | 任何 | 无(CSR) |
示例框架 | 11ty | Astro、Elder | Marko、Qwik、Hydrogen | Next、Remix | Create React App |