前端架构的性能优化策略

本文介绍了一些可以使前端应用程序加载更快并提供良好用户体验的技术。

我们想看的是前端的整体结构。如何首先加载必要的资源,并尝试提高缓存中资源的命中率?

本文不会过多讨论后端如何交付资源,您的页面是否需要客户端应用程序,或者如何优化应用程序的呈现时间。

1前言

我将应用程序加载过程分为三个阶段:

最初呈现——用户开始看到内容需要多长时间?

应用程序加载——用户开始使用应用程序需要多长时间?

从下一页——移到下一页需要多长时间?

1000.jpg

2初始渲染

在浏览器的初始渲染完成之前,用户看不到任何东西。至少,需要为页面呈现加载HTML文档,但是大多数时候需要加载其他资源,例如CSS和JavaScript文件。一旦内容可用,浏览器就可以开始在屏幕上绘制内容。

在本文中,我将使用网页测试瀑布图。您网站的请求瀑布可能如下所示。

1000.jpg

HTML文档将加载一堆额外的文件,并在加载这些文件后呈现页面。请注意,CSS文件是并行加载的,因此每个额外的请求不会显著增加延迟。

(注意:HTTP/2现在在英国政府中启用,因此资产域可以重用现有的www.gov.uk连接!稍后将详细讨论服务器连接。)

3减少对分块渲染的请求

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

您可以通过以下方式解决这个问题:

将脚本标签放在正文标签的底部;

使用异步来异步加载脚本;

如果需要同步加载,则内嵌小的JS或CSS代码片段。

4避免阻塞呈现的顺序请求链

减慢站点速度的不一定是阻止渲染的请求数量。更重要的是,每个资源的下载量都太大,而且有太多浏览器需要加载资源的情况。

如果浏览器在请求完成后才发现需要加载文件,那么您可以使用同步请求链来提高效率。这有许多可能的原因:

CSS中的@导入规则;

网络字体;在CSS文件中引用;

JavaScript注入链接或脚本标签。

看看这个例子:

1000.jpg

该网站使用@import将谷歌字体加载到它的一个CSS文件中。这意味着浏览器需要逐一发出以下请求:

文档超文本标记语言

应用软件

谷歌字体

谷歌字体文件(未显示在瀑布图中)

为了解决这个问题,首先把对谷歌字体CSS的请求从@import移动到HTML文档中的一个链接标签。这从链条上去掉了一个环节。

为了进一步加快速度,请直接在您的HTML或CSS文件中写入内联谷歌字体 CSS 文件

(请注意,谷歌字体的CSS响应取决于用户代理。如果您使用IE8发出请求,CSS将引用一个EOT文件;IE11会得到一个woff文件,现代浏览器会得到一个WOF2文件。但是,如果您认为旧的浏览器可以使用系统字体,您可以简单地复制和粘贴CSS文件的内容。)

即使在页面开始呈现之后,用户可能仍然不能对页面做任何事情,因为在加载字体之前不会显示任何文本。这可以通过字体显示交换来避免,这将默认为谷歌字体。

有时消除请求链是不可行的。在这些情况下,您可以考虑预加载或预连接标签。例如,在提出实际的CSS请求之前,可以将上述网站连接到fonts.googleapis.com。

5重复使用服务器连接来加速请求

建立新的服务器连接通常需要服务器和浏览器之间的三次往返:

域名系统查询;

建立一个传输控制协议连接;

建立一个SSL连接。

一旦连接准备好,至少还需要一次往返来发送请求和下载响应。

下面的瀑布图显示连接已经开始到四个不同的服务器:hostgator.com,optimize.com,googletagmanager.com和googelapis.com。

但是对同一服务器的后续请求可以复用现有连接。因此,加载base.css或index1.css非常快,因为它们也托管在hostgator.com。

1000.jpg

6减小文件大小并使用CDN

除了文件大小之外,还有两个影响请求时间的因素在您的控制范围内:资源大小和服务器位置。

最大限度地减少发送给用户的数据量,并确保数据被压缩(例如使用brotli或gzip)。

内容交付网络在许多地理位置提供服务器,因此其中一个可能就在您的用户附近。用户可以连接到附近的CDN服务器,而不必连接到您的应用程序中心服务器。这意味着服务器往返时间将大大缩短。这对于静态资产(如CSS、JavaScript和图像)尤其方便,因为它们易于分发。

7使用服务人员跳过网络

服务人员允许您在进入网络之前拦截请求。这意味着你的第一批绘制实际上是即时的!

1000.jpg

当然,这只有在网络不需要发送响应时才有效。您需要一个缓存的响应,因此用户只会从您的应用程序的第二次加载中受益。

下面的服务工作者缓存了呈现页面所需的HTML和CSS。当应用程序再次加载时,它将尝试提供缓存的资源,或者在不可用时回滚到网络。

self.addEventListener(“安装”),异步e={

cache . open(' v1 ')。然后(函数(缓存){

返回缓存. addAll(['/app ','/app . CSS ']);

});

});

self.addEventListener('fetch ',事件={

event.respondWith(

缓存.匹配(事件.请求)。然后(缓存响应={

返回缓存响应||提取(event . request);

})

);

});

阅读本指南,了解有关使用服务人员预加载和缓存资源的更多信息:

https://developer . Google.com/web/ILT/pwa/cache-files-with-service-worker

8应用程序加载

好了,现在用户可以看到一些东西。在开始使用你的应用之前,他们还需要什么?

加载应用程序代码

加载页面的基本数据

加载附加数据和图像

1000.jpg

请注意,延迟渲染的不仅仅是从网络加载数据的过程。加载代码后,浏览器需要解析、编译并执行它。

9包拆分:只加载必要的代码

数据包分割允许您只加载当前页面所需的代码,而不是整个应用程序。数据包分割还意味着您可以缓存其中的一些部分,即使其他部分已经更改并需要重新加载。

通常,代码被分成三种不同类型的文件:

网页特定代码

共享应用程序代码

很少改变的第三方模块(非常适合缓存!)

Webpack可以使用optimization.splitChunks来自动拆分共享代码,以减少下载的数据总量。确保启用运行时块来稳定块哈希并受益于长期缓存。Ivan Akulov写了一篇关于Webpack代码拆分和缓存的深度指南:

https://developer . Google.com/web/fundamentals/performance/web pack/use-long-term-cache

专有代码的拆分不能自动完成。您需要确定哪些数据可以单独加载。通常这是一个特定的路径或一组页面。使用动态导入来延迟加载这些代码:

https://webpack.js.org/guides/code-splitting/#dynamic-imports

使用包拆分后,加载应用程序时会发出更多请求。然而,只要请求可以并行发送,就不会有大问题,尤其是当您的站点通过HTTP/2提供服务时。下面瀑布图中的前三个请求是一个例子:

1000.jpg

然而,这个瀑布图也按顺序显示了两个请求。这些块仅由该页面需要,并通过import()调用动态加载。

如果您知道页面需要这些块,您可以插入一个预加载的链接标签来解决这个问题。

https://www . debug bear.com/blog/resource-tips-rel-preload-prefetch-preconnect # preload

1000.jpg

但是,您将会看到,通过这种方法减少的页面加载时间与总加载时间相比可能微不足道。

此外,使用预加载策略有时会适得其反,因为加载其他更重要的文件时可能会有延迟。请查看安迪·戴维斯关于预载字体的文章,你会知道在加载CSS阻止渲染之前加载字体也会影响初始加载过程。

https://and ydavies . me/blog/2019/02/12/preload-font-and-the-谜题-of-priorities/

10加载页面数据

您的应用程序可能会显示一些数据。以下是一些技巧,您可以使用它们来尽早加载数据,避免渲染延迟。

在开始加载数据之前,不要等待包

这是顺序请求链的一个特例:首先加载应用程序包,然后代码请求页面数据。

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

将页面数据嵌入到超文本标记语言文档中;

通过文档中的内联脚本启动数据请求。

将数据嵌入到HTML中可以确保您的应用程序不必等待数据加载。这也降低了应用程序的复杂性,因为您不必处理加载状态。

但是,如果获取数据的过程会显著延迟您的文档响应,这不是一个好主意,因为它会延迟您的初始呈现。

在这种情况下,或者如果您通过服务工作者提供缓存的HTML文档,您可以将内联脚本嵌入到HTML中以加载数据。您可以将其用作全球承诺,如下所示:

window . UserDataColonSe=fetch('/me ')

然后,当数据准备好了,您的应用程序可以立即开始渲染。对于这两种技术,在应用程序开始呈现之前,您需要知道页面必须加载什么数据。这对于与用户相关的数据(用户名、通知等)来说通常很容易。),但对于特定于页面的内容来说更麻烦。考虑寻找最重要的页面,并为它们编写自定义逻辑。

在等待不必要的数据时,不要阻止渲染

有时,生成页面数据的过程需要缓慢而复杂的后端逻辑。在这种情况下,您可以先加载一个更简单的数据版本,只要数据足以让您的应用程序工作。

例如,分析工具可以在加载图表数据之前先加载所有图表的列表。这使用户能够立即找到他们感兴趣的图表,也有助于将后端请求分散到不同的服务器上。

1000.jpg

避免顺序数据请求链

这可能与我之前关于在第二个请求中加载不必要的数据的观点相冲突,但是如果每个完成的请求没有向用户显示更多的信息,那么应该避免顺序请求链。

在请求一个用户所属的团队列表之前,最好返回团队列表和用户信息,而不是先请求用户的登录身份。您可以使用GraphQL来做到这一点,但是自定义用户呢?IncludeTeams=真端点也很有用。

11服务器渲染

服务器呈现意味着在服务器上预先呈现您的应用程序,并使用整页的HTML响应文档请求。这意味着客户端可以看到完整呈现的页面,而无需等待加载额外的代码或数据!

由于服务器只向客户端发送静态的HTML,您的应用程序还不能交互。您需要加载应用程序,重新运行呈现逻辑,然后将必要的事件侦听器附加到DOM。

如果您希望用户及时看到非交互式内容,请使用服务器端呈现。如果您可以在服务器上缓存呈现的HTML,并使其对所有用户可用,而不会延迟初始文档请求,那么您也可以使用服务器端呈现。例如,如果您使用“反应”来呈现博客文章,服务器端呈现是非常合适的。

阅读本文,了解如何将服务工作者和服务器渲染结合起来。

https://micaljanazek.com/blog/combine-pwa-and-同构-渲染

12下一页

在某个时刻,用户将与您的应用程序进行交互,并转到下一页。打开初始页面后,您可以控制浏览器中发生的事情,以便为此时的下一次交互做准备。

预取资源

如果您预加载下一页所需的代码,就可以消除用户跳转时的延迟。使用预回迁链接标签或使用webpackPrefetch进行动态导入:

导入(/* webpackPrefetch: true,WebPackChankName : ' todo-list ' */'。/TodoList ')

请记住您正在使用的用户数据和带宽信息,尤其是当用户使用移动连接时。如果他们使用移动版本的网站或启用数据保存模式,他们可以降低预加载的热情。针对用户最有可能需要应用的部件制定适当的策略。

重用已加载的数据

在应用程序中本地缓存Ajax数据,并使用它来避免将来的请求。如果用户从团队列表页面跳转到编辑团队页面,获取的数据可以被重用,实现即时跳转。

请注意,如果您的实体经常被其他用户编辑,这种方法是无用的,您下载的数据可能会很快过期。在这些情况下,在获取最新数据时,请考虑首先将现有数据显示为只读。

摘要

本文介绍了在加载过程中不同时间可能会降低页面速度的许多因素。使用诸如ChromeDevTools、网页测试和灯塔等工具来确定哪些因素出现在您的应用程序中。

事实上,你很难在所有方面做好优化工作。找出对用户影响最大的因素,并集中精力解决它们。

当我写这篇文章的时候,我意识到一件事,我曾经坚信提出许多不同的请求对性能没有好处。在过去,当每个请求需要一个单独的连接时就是这种情况,在浏览器的每个域中只允许几个连接。然而,现在使用HTTP/2的浏览器不再是这种情况。

此外,我们有很好的理由分割请求。这样,只有必要的资源可以被加载,缓存的内容可以得到更好的利用,因为我们只需要重新加载已更改的文件。