加快前端速度。当许多服务器请求都很好时

本文介绍了一些加快加载前端应用程序以实现快速响应用户界面的方法。

我们将讨论前端的一般体系结构,如何预加载必要的资源并增加它们在缓存中的可能性。我们将讨论如何从后端提供资源,以及何时可以将自己限制为静态页面而不是交互式客户端应用程序。

下载过程分为三个阶段。对于每个阶段,我们都制定了提高生产率的一般策略:

  1. 初始渲染:用户需要多长时间才能看到至少一些东西
    • 减少渲染阻止请求
    • 避免顺序链
    • 重用服务器连接
    • 即时渲染的服务人员
  2. : ,
    • . .
    • ,


  3. :
    • ,


在初始渲染之前,用户在屏幕上看不到任何东西。我们需要什么渲染?至少上载HTML文档,并在大多数情况下上载其他资源,例如CSS和JavaScript文件。一旦可用,浏览器就可以开始某种渲染。本文通篇

提供了WebPageTest图表。您网站的查询顺序可能看起来像这样。



HTML文档加载了许多其他文件,并且在下载文件后呈现了页面。请注意,CSS文件是彼此并行加载的,因此每个其他请求不会增加明显的延迟。

(注意:在屏幕截图中,gov.uk是一个示例,其中HTTP / 2现在已启用以便资源域可以重用现有连接。请参阅下面的服务器连接。)

减少渲染阻止请求


样式表和(默认情况下)脚本会阻止其下方任何内容的呈现。

有几种解决方法:

  • 将脚本标签移到正文底部
  • 使用异步模式下载脚本 async
  • 如果应按顺序加载JS或CSS,则最好将它们嵌入小片段中

避免与阻止渲染的顺序请求进行对话


渲染站点的延迟不一定与阻止渲染的大量请求相关。更重要的是每个资源的大小及其下载的开始时间。也就是说,浏览器突然意识到需要下载该资源的时刻。

如果浏览器仅在完成另一个请求后才检测到需要下载文件,则存在一系列请求。可能由于多种原因而形成:

  • @importCSS 规则
  • CSS文件引用的Web字体
  • 可下载的JavaScript或脚本标签

看一下这个例子:



这个站点上的一个CSS文件通过rule加载Google字体@import这意味着浏览器必须轮流执行以下请求:

  1. HTML文件
  2. CSS应用
  3. Google字体的CSS
  4. Google字体Woff文件(未在图中显示)

要解决此问题,请先将Google字体CSS请求从标记@import移到HTML文档中的链接。因此,我们将链接缩短了一个链接。

为了进一步加快速度,请将Google字体CSS直接嵌入 HTML或CSS文件中。

(请注意,来自Google字体服务器的CSS响应取决于用户代理行。如果您使用IE8进行请求,则CSS将引用EOT文件,IE11浏览器将接收woff文件,而现代浏览器将接收woff2文件。如果您同意旧版浏览器只能使用系统字体,您只需将CSS文件的内容复制并粘贴到您自己)。

即使在开始渲染之后,用户也不太可能与页面进行交互,因为需要加载字体以显示文本。我想避免这种额外的网络延迟。swap参数在这里很有用,它允许您将其font-display与Google字体一起使用,并在本地存储字体。

有时查询链无法解析。在这种情况下,您可能需要考虑preload或preconnect标签例如,以上示例中的网站可能fonts.googleapis.com在实际CSS请求到达之前就已连接。

重用服务器连接以加快请求


要建立与服务器的新连接,通常需要在浏览器和服务器之间交换三个数据包:

  1. DNS查询
  2. 建立TCP连接
  3. 建立SSL连接

建立连接后,还需要至少交换一个数据包才能发送请求和接收响应。

下面图表所示,我们开始与四个不同的服务器的连接:hostgator.comoptimizely.comgoogletagmanager.com,和googelapis.com

但是,后续的服务器请求可能会重用现有的连接下载base.css下载的index1.css速度更快,因为它们位于与服务器hostgator.com建立连接的同一服务器上。



减小文件大小并使用CDN


您控制两个影响查询执行时间的因素:资源文件的大小和服务器的位置。

向用户发送尽可能少的数据,并确保将其压缩(例如,使用brotli或gzip)。

内容交付网络(CDN)的服务器遍布世界各地。用户可以连接到更近的CDN服务器,而不必连接到中央服务器。因此,分组交换将更快。这特别适用于静态资源,例如CSS,JavaScript和图像,因为它们很容易通过CDN进行分发。

与服务人员一起消除网络延迟


服务人员可让您在将请求发送到网络之前拦截请求。这意味着答案几乎马上就来了



当然,这仅在您真的不需要从网络接收数据的情况下才有效。答案应该已经被缓存,因此,好处仅会在第二次下载应用程序时出现。

下面的服务人员缓存呈现页面所需的HTML和CSS。当应用程序再次加载时,它会尝试自行发出缓存的资源-并仅在不可用时访问网络。

self.addEventListener("install", async e => {
 caches.open("v1").then(function (cache) {
   return cache.addAll(["/app", "/app.css"]);
 });
});

self.addEventListener("fetch", event => {
 event.respondWith(
   caches.match(event.request).then(cachedResponse => {
     return cachedResponse || fetch(event.request);
   })
 );
});

本指南中,详细说明了如何使用服务工作者预加载和缓存资源。

下载申请书


因此,用户在屏幕上看到了一些东西。他使用该应用程序需要采取什么进一步的步骤?

  1. 下载应用程序代码(JS和CSS)
  2. 下载页面所需的数据
  3. 下载其他数据和图像



请注意,不仅从网络下载数据可能会延迟渲染。加载代码后,浏览器应进行分析,编译和执行。

仅下载必要的代码,并最大化缓存中的命中次数


“破坏软件包”表示仅下载当前页面所需的代码,而不下载整个应用程序。这也意味着即使其他部分已更改并且需要重新加载,也可以缓存软件包的某些部分。

通常,代码分为以下几部分:

  • 特定页面的代码(特定于页面)
  • 通用应用代码
  • 很少更改的第三方模块(非常适合缓存!)

Webpack可以自动执行此优化,破坏代码并减少总体负载。使用optimization.splitChunks对象将代码分为几部分将运行时(运行时)分成一个单独的文件:这样,您可以从长期缓存中受益。伊万·阿库洛夫(Ivan Akulov)撰写了详细的指南,介绍如何将程序包分成单独的文件并在Webpack中进行缓存

无法自动为特定页面分配代码。您必须手动识别可以单独下载的那些部分。通常,这是特定的路径或页面集。使用动态导入来延迟加载此代码。

将整个程序包分成几部分将增加下载应用程序的请求数量。但是,如果并行执行请求,这并不是一个大问题,特别是如果使用HTTP / 2协议加载站点。您可以在下图中的前三个查询中看到这一点:



但是,在图中也可以看到两个连续的查询。这些片段仅在此特定页面才需要,它们通过进行动态加载import()

您可以尝试通过插入标签preload preload来解决问题



但是我们看到页面总加载时间增加了。

资源预加载有时会适得其反,因为它会延迟更重要文件的加载。读安迪·戴维斯(Andy Davis)关于预加载字体以及此过程如何阻止页面渲染开始的文章

加载页面数据


您的应用程序可能应该显示一些数据。您可以使用以下提示尽早下载此数据,而不会造成不必要的渲染延迟。

开始下载数据之前,请勿等待软件包的完整下载。


这是一系列顺序请求的特例:下载整个应用程序包,然后此代码为页面请求必要的数据。

有两种方法可以避免这种情况:

  1. 将数据嵌入HTML文档
  2. 使用文档内部的内置脚本运行数据请求

将数据嵌入HTML中可确保应用程序不等待加载。由于您不需要处理引导状态,因此它还降低了系统的复杂性。

但是,如果这种技术延迟了初始渲染,那么这并不是一个好主意。

在这种情况下,如果您要通过服务工作者提交缓存的HTML文档,也可以使用内置脚本来下载此数据。您可以将其作为全局对象使用,这是一个承诺:

window.userDataPromise = fetch("/me")

如果数据准备就绪,并且在这种情况下,应用程序可以立即开始渲染或等待直到准备就绪。

使用这两种方法时,您需要事先知道页面将在应用程序开始呈现之前加载哪些数据。对于与用户相关的数据(用户名,通知等),这通常很明显,但是对于特定页面特定的内容则更加困难。突出显示最重要的页面并为它们编写自己的逻辑也许是有意义的。

等待不相关的数据时不要阻塞渲染


有时,要生成数据,您需要在后端运行缓慢的复杂逻辑。在这种情况下,如果足以使应用程序正常运行和交互,则可以尝试首先下载数据的简单版本。

例如,分析工具可以在加载数据之前首先下载所有图表的列表。这使用户可以立即搜索他感兴趣的图表,还有助于将后端请求分发到不同的服务器。



避免连续数据查询


这可能与上一段相反,那就是最好将非必需数据放在单独的请求中。因此,您应该澄清:如果每个完成的请求都不会导致向用户显示更多信息,请避免使用顺序数据请求链

与其先查询哪个用户已登录然后再请求其组列表,不如先返回组列表以及第一个请求中的用户信息。您可以为此使用GraphQL,但是端点user?includeTeams=true也可以正常工作。

服务器端渲染


服务器端渲染意味着预先渲染应用程序,因此应客户端请求返回整页HTML。客户端看到页面已完全呈现,而无需等待其他代码或数据加载!

由于服务器仅向客户端发送静态HTML,因此应用程序此时无法交互。您需要下载应用程序本身,启动呈现逻辑,然后将必要的事件侦听器连接到DOM。

如果查看非交互式内容本身很有价值,请使用服务器端呈现。将呈现的HTML缓存在服务器上并立即将其立即返回给所有用户也很不错。例如,使用React显示博客帖子时,服务器端渲染效果很好。

Mikhail Yanashek的这篇文章描述了如何结合服务工作者和服务器端渲染。

下一页


在某个时候,用户将要按下一个按钮并转到下一页。从打开起始页面的那一刻起,您就可以控制浏览器中发生的事情,因此可以为下一次交互做准备。

资源预载


如果您预加载了下一页所需的代码,则当用户开始导航时,延迟消失。使用预取标签webpackPrefetch动态导入:

import(
    /* webpackPrefetch: true, webpackChunkName: "todo-list" */ "./TodoList"
)

考虑流量和带宽给用户带来的负担,特别是如果用户通过移动连接连接时。如果有人下载了该网站的移动版本,并且数据存储模式处于活动状态,则合理地进行较小的预加载是合理的。

从战略上考虑用户之前需要的应用程序部分。

重用已经下载的数据


在应用程序本地缓存数据,并使用它避免将来的请求。如果用户从其组列表转到“编辑组”页面,则可以立即进行转换,从而重复使用以前下载的有关该组的数据。

请注意,如果该对象经常由其他用户编辑,并且下载的数据可能已过时,则此方法将无效。在这些情况下,可以选择先显示现有的只读数据,同时执行对更新数据的请求。

结论


本文列出了许多因素,这些因素可能会在加载过程的不同阶段降低您的页面速度。Chrome DevToolsWebPageTestLighthouse之类的工具将帮助您确定哪些因素会影响您的应用程序。

实际上,很少有优化立即向各个方向发展。我们需要找出对用户影响最大的内容,并专注于此。

在撰写本文时,我意识到了一件重要的事情:我根深蒂固地相信许多单独的服务器请求都不利于性能。过去就是这种情况,当时每个请求都需要一个单独的连接,而浏览器每个域只允许几个连接。但是对于HTTP / 2和现代浏览器,情况已不再如此。

有很多赞成将应用程序拆分为多个部分的理由(使用乘法查询)。这样一来,您只能下载必要的资源,并且最好使用缓存,因为只有更改过的文件才需要重新加载。

All Articles