无服务器环境中的服务器渲染

该材料的作者(我们正在翻译的译本)是Webiny项目的创始人之一-该Webiny项目是基于React,GraphQL和Node.js的无服务器CMS。 他说,支持多租户无服务器云平台是一项具有特定任务的业务。 已经写了许多文章,其中讨论了用于优化Web项目的标准技术。 其中包括服务器渲染,高级Web应用程序开发技术的使用,各种改进应用程序构建的方法等等。 一方面,本文与其他相似,另一方面,与它们不同。 事实是它致力于优化在无服务器环境中运行的项目。



准备工作


为了进行评估以帮助确定项目问题,我们将使用webpagetest.org 。 借助此资源,我们将满足请求并收集有关各种操作的执行时间的信息。 这将使我们能够更好地了解用户在处理项目时的感受。

我们对“第一视图”指标特别感兴趣,也就是说,从第一次访问该网站的用户加载网站需要多长时间。 这是一个非常重要的指标。 事实是,浏览器缓存能够隐藏许多Web项目的瓶颈。

反映站点负载特征的指标-问题识别


看一下下面的图表。


网络项目的新旧指标分析

在这里,最重要的指标可以识别为“开始渲染的时间”-开始渲染之前的时间。 如果您仔细查看此指示器,可以看到仅是为了开始呈现页面,在项目的旧版本中,它花费了将近2秒钟。 原因在于单页应用程序(SPA)的本质。 为了在屏幕上显示此类应用程序的页面,您首先需要加载大量的JS捆绑包(页面加载的这一阶段在下图中标记为1)。 然后,需要在主线程(2)中处理此捆绑包。 只有在此之后,浏览器窗口中才会出现某些内容。


(1)下载JS捆绑软件。 (2)在主线程中等待捆绑处理

但是,这只是图片的一部分。 主线程处理完JS包后,它会向Gateway API发出多个请求。 在页面处理的此阶段,用户会看到旋转的加载指示器。 景象不是最愉快的。 但是,用户尚未看到任何页面内容。 这是页面加载过程的情节提要。


页面加载

所有这些表明,访问这样的站点的用户与他一起工作时没有特别愉快的感觉。 即,他被迫在空白页上查看2秒钟,然后再看一秒钟-在下载指示器上。 由于在加载和处理后执行了JS捆绑API请求,因此将这一秒添加到页面准备时间中。 这些查询对于加载数据是必要的,并因此显示完成的页面。


页面加载

如果项目托管在常规VPS上,则完成这些API请求所需的时间大部分是可以预测的。 但是,在无服务器环境中运行的项目会受到臭名昭著的“冷启动”问题的影响。 对于Webiny云平台,情况甚至更糟。 AWS Lambda功能是VPC(虚拟私有云)的一部分。 这意味着对于该功能的每个新实例,都需要初始化ENI(弹性网络接口,弹性网络接口)。 这显着增加了功能的冷启动时间。

以下是在VPC内和VPC外部加载AWS Lambda功能的一些时间表。


VPC内部和VPC外部的AWS Lambda函数负载分析(图像来自此处

由此可以得出结论,在VPC内部启动该功能的情况下,冷启动时间将增加10倍。

另外,这里还必须考虑一个因素-网络数据传输延迟。 它们的持续时间已包含在执行API请求所需的时间中。 请求由浏览器启动。 因此,事实证明,到API响应这些请求时,添加了将请求从浏览器获取到API所需的时间,以及响应从API到达浏览器所花费的时间。 这些延迟发生在每个请求期间。

优化任务


在上述分析的基础上,我们制定了一些需要解决的任务以优化项目。 它们是:

  • 提高执行API请求的速度或减少阻止渲染的API请求的数量。
  • 减小JS捆绑包的大小或将此捆绑包转换为页面输出不需要的资源。
  • 解锁主线程。

问题方法


以下是解决我们考虑的问题的几种方法:

  1. 为了加快执行速度而进行的代码优化。 这种方法需要大量的努力,它具有很高的成本。 通过这种优化获得的收益值得怀疑。
  2. 增加可用于AWS Lambda功能的RAM数量。 这很容易做到,这种解决方案的成本在中高之间。 应用该解决方案只能带来很小的积极影响。
  3. 使用某种其他方式来解决问题。 是的,那一刻我们还不知道这种方法是什么。

最后,我们选择了该列表中的第三项。 我们这样推断:“如果我们绝对不需要API调用怎么办? 如果我们完全不用JS捆绑包怎么办? 这将使我们能够解决该项目的所有问题。”


我们发现有趣的第一个想法是创建渲染页面的HTML快照并将其与用户共享。

尝试失败


Webiny Cloud是支持Webiny站点的基于AWS Lambda的无服务器基础架构。 我们的系统可以检测机器人。 当事实证明该请求已由漫游器完成后,该请求将重定向到Puppeteer实例,该实例使用没有用户界面的Chrome呈现页面。 页面的现成HTML代码将发送到机器人。 这样做主要是出于SEO的原因,因为许多机器人不知道如何执行JavaScript。 我们决定使用相同的方法来准备面向普通用户的页面。


这种方法在缺乏JavaScript支持的环境中效果很好。 但是,如果您尝试将预渲染的页面提供给其浏览器支持JS的客户端,则会显示该页面,但是在下载JS文件后,React组件根本不知道将它们挂载在何处。 这会在控制台中产生一堆错误消息。 结果,这样的决定不适合我们。

SSR简介


服务器端渲染(SSR)的优势在于,所有API请求均在本地网络内执行。 由于它们由VPC内运行的某个系统或功能处理,因此执行从浏览器到资源后端的请求时出现的延迟是不典型的。 尽管在这种情况下,仍然存在“冷启动”的问题。

使用SSR的另一个好处是,我们为客户端提供了页面的HTML版本,在使用该页面时,在加载JS文件后,React组件不会出现安装问题。

最后,我们不需要很大的JS包。 此外,我们可以不使用API​​来显示页面。 捆绑包可以异步加载,这不会阻塞主线程。

通常,可以说服务器渲染似乎应该已经解决了我们的大多数问题。

这是应用服务器端渲染后站点分析的外观。


应用服务器渲染后的网站指标

现在不执行API请求,并且可以在加载大型JS捆绑包之前看到该页面。 但是,如果仔细查看第一个请求,您会发现从服务器获取文档大约需要2秒钟。 让我们来谈谈。

TTFB问题


在这里,我们讨论TTFB度量(到第一个字节的时间,到第一个字节的时间)。 以下是第一个请求的详细信息。


首次请求详细信息

要处理此第一个请求,我们需要执行以下操作:启动Node.js服务器,执行服务器渲染,发出API请求并执行JS代码,然后将最终结果返回给客户端。 这里的问题是所有这些平均需要1-2秒。

我们的服务器执行服务器渲染,需要完成所有这些工作,只有在此之后,它才能将响应的第一个字节传输到客户端。 这导致浏览器有很长的时间等待对请求的响应开始的事实。 结果,现在对于页面的输出,您需要进行的工作量几乎与以前一样。 唯一的区别是,此工作不是在客户端执行,而是在服务器渲染过程中在服务器上执行。

在这里,您可能对“服务器”一词有疑问。 我们一直都在谈论无服务器系统。 这个“服务器”来自哪里? 我们当然尝试在AWS Lambda函数中渲染服务器渲染。 但是事实证明,这是一个非常消耗资源的过程(特别是,为了获得更多的处理器资源,必须大量增加内存量)。 另外,我们已经提到的“冷启动”问题也被添加到这里。 结果,理想的解决方案是使用Node.js服务器来加载站点材料并在服务器端进行渲染。

让我们回到使用服务器端渲染的后果。 看下面的情节提要。 可以很容易地看出,它与在项目研究中获​​得的并没有特别不同,后者是在客户端上呈现的。


使用服务器端渲染时的页面加载

用户被迫查看空白页面2.5秒钟。 真伤心

尽管看了这些结果,但人们可能会认为我们绝对没有取得任何成就,实际上并非如此。 我们有该页面的HTML快照,其中包含我们所需的一切。 该快照已准备好与React一起使用。 同时,在客户端上处理页面期间,无需执行任何API请求。 所有必需的数据已经嵌入到HTML中。

唯一的问题是创建此HTML快照要花费太多时间。 在这一点上,我们可以花更多的时间来优化服务器渲染,或者只是缓存其结果,并从客户端(例如Redis缓存)为页面提供快照。 我们就是这样做的。

缓存服务器渲染结果


用户访问Webiny网站后,我们首先检查集中化的Redis缓存,以查看页面是否存在HTML快照。 如果是这样,我们给用户一个缓存中的页面。 平均而言,这将TTFB降低到200-400毫秒。 引入缓存之后,我们才开始注意到项目性能的显着提高。


使用服务器端呈现和缓存时的页面加载

即使是第一次访问该站点的用户,也可以在不到一秒钟的时间内看到该页面的内容。

让我们看一下瀑布图的外观。


应用服务器端渲染和缓存后的网站指标

红线表示800毫秒的时间戳。 这是页面内容完全加载的地方。 此外,在这里您可以看到JS捆绑软件的加载时间约为1.3秒。 但这不会影响用户查看页面的时间。 同时,您无需进行API调用并加载主线程即可显示页面。

请注意以下事实,即有关加载JS软件包,执行API请求以及在主线程中执行操作的临时指示符在准备工作页面时仍起着重要作用。 为了使页面“交互”,需要投入时间和资源。 但这首先没有作用,对于搜索引擎机器人而言,其次,在用户之间创造了“快速页面加载”的感觉。

假设页面是“动态的”。 例如,如果正在查看页面的用户已登录,它将在标题中显示一个链接以访问用户帐户。 服务器端渲染后,通用页面将发送到浏览器。 即-向未登录的用户显示的内容。 该页面的标题将更改,反映出只有在加载JS捆绑包和进行API调用之后用户才能登录的事实。 在这里,我们正在处理TTI指标(“互动时间”,即首次互动的时间)。

几周后,我们发现代理服务器未在需要的情况下关闭与客户端的连接,以防服务器渲染是作为后台进程启动的。 实际上,仅一行代码的更正导致TTFB指示器降低到50-90 ms的事实。 结果,该站点现在大约600毫秒后开始在浏览器中显示。

但是,我们面临另一个问题...

缓存失效问题


“在计算机科学中,只有两件复杂的事情:缓存失效和实体命名。”
菲尔·卡尔顿

缓存无效确实是一项非常困难的任务。 怎么解决呢? 首先,通常可以通过为缓存的对象设置非常短的存储时间(TTL,生存时间,生存期)来更新缓存。 有时这将导致页面加载速度比平常慢。 其次,您可以基于某些事件创建缓存失效机制。

在我们的案例中,使用30秒的非常小的TTL解决了此问题。 但是我们也意识到有可能从缓存中为客户提供过时的数据。 在客户端接收到此类数据时,缓存将在后台进行更新。 因此,我们解决了AWS Lambda函数常见的问题,例如延迟和“冷启动”。

运作方式如下。 用户访问Webiny网站。 我们正在检查HTML缓存。 如果页面有屏幕截图,我们会将其提供给用户。 图片的年龄甚至可能只有几天。 通过在几百毫秒内将旧快照传递给用户,我们同时启动了创建新快照和更新缓存的任务。 由于我们创建了一种机制,因此我们始终拥有一定数量的已经运行并准备工作的AWS Lambda函数,通常需要花费几秒钟来完成此任务。 因此,在创建新映像期间,我们不必花时间在功能的冷启动上。

结果,我们总是将页面从缓存返回到客户端,并且当缓存的数据使用期限达到30秒时,缓存的内容就会更新。

缓存绝对是我们仍然可以改进的领域。 例如,我们正在考虑在用户发布页面时自动更新缓存的可能性。 但是,这种高速缓存更新机制也不理想。

例如,假设资源的主页显示三个最新的博客文章。 如果在发布新页面时更新了缓存,则从技术角度来看,发布后将仅生成该新页面的缓存。 主页的缓存将过时。

我们仍在寻找改善项目缓存系统的方法。 但是到目前为止,重点一直放在解决现有的性能问题上。 我们认为,在解决这些问题方面我们做得很好。

总结


首先,我们使用了客户端渲染。 然后,平均而言,用户可以在3.3秒内看到该页面。 现在,该数字已降至约600毫秒。 同样重要的是我们现在省去了下载指示器。

为了获得此结果,主要允许我们使用服务器渲染。 但是,如果没有一个好的缓存系统,结果就是计算只是从客户端转移到服务器。 这就导致了这样一个事实,即用户查看页面所需的时间没有太大变化。

服务器渲染的使用具有另一个积极的品质,前面没有提到。 我们正在谈论的事实是,它可以更轻松地在弱小的移动设备上查看页面。 准备在此类设备上查看的页面的速度取决于其处理器的适度功能。 服务器渲染允许您从其中除去部分负载。 应该注意的是,我们没有对此问题进行过专门研究,但是我们拥有的系统应该有助于改善手机和平板电脑上该网站的观看情况。

通常,可以说实现服务器渲染不是一件容易的事。 而且我们使用无服务器环境这一事实只会使这项任务复杂化。 解决问题的方法需要更改代码,增加基础结构。 我们需要创建一个设计良好的缓存机制。 但是作为回报,我们得到了很多好处。 最重要的是,我们网站的页面现在正在加载,并且准备工作的速度比以前快得多。 我们相信我们的用户会喜欢它。

亲爱的读者们! 您是否使用缓存和服务器渲染技术来优化您的项目?

Source: https://habr.com/ru/post/zh-CN459306/


All Articles