资源加载优化(七)

943 阅读16分钟

1.图片延迟加载

什么是延迟加载

首先来想象一个场景,当浏览一个内容丰富的网站时,比如电商的商品列表页、主流视频网站的节目列表等,由于屏幕尺寸的限制,每次只能查看到视窗中的那部分内容,而要浏览完页面所包含的全部信息,就需要滚动页面,让屏幕视窗依次展示出整个页面的所有局部内容

而易见,对于首屏之外的内容,特别是图片和视频,一方面由于资源文件很大,若是全部加载完,既费时又费力,还容易阻塞渲染引起卡顿;另一方面,就算加载完成,用户也不一定会滚动屏幕浏览到全部页面内容,如果首屏内容没能吸引住用户,那么很可能整个页面就将遭到关闭

既然如此,本着节约不浪费的原则,在首次打开网站时,应尽量只加载首屏内容所包含的资源,而首屏之外涉及的图片或视频,可以等到用户滚动视窗浏览时再去加载

以上就是延迟加载优化策略的产生逻辑,通过延迟加载“非关键”的图片及视频资源,使得页面内容更快地呈现在用户面前。这里的“非关键”资源指的就是首屏之外的图片或视频资源,相较于文本、脚本等其他资源来说,图片的资源大小不容小觑

实现图片延迟加载

传统方式

Intersection Observer 方式

此外,若将首屏视窗边界线作为延迟加载触发的阈值,其实并非最佳的性能考虑

更理想的做法是,在延迟加载的媒体资源到达首屏边界之前设置一个缓冲区,以便媒体资源在进入视窗之前就开始进行加载

例如在使用Intersection Observer方式实现延迟加载判断时,可以通过配置options对象中的rootMargin属性来建立缓冲区:

const lazyImageObserver = new IntersectionObserver((entries, observer) => {
  // 此处省略延迟加载的具体处理流程
}, {
  rootMargin: '0 0 256px 0'
})

观察可知 rootMargin 的值与 CSS 中 margin 属性值类似,上述代码中在屏幕视窗下设置了一个宽度为 256px 的缓冲区,这意味着当媒体元素距离视窗下边界小于 256px 时,回调函数就会执行开始资源的请求加载。而对于使用滚动事件处理来实现延迟加载的传统实现方式,也只需要更改 getBoundingClientRect 的设置,包括进入一个缓冲区即可实现类似的效果

加载注意事项

对图像等资源的延迟加载,从理论上看必然会对性能产生重要的影响,但在实现过程中有许多细节需要注意,稍有差池都可能就会产生意想不到的结果。因此,总结以下几点注意事项

资源占位

当延迟加载的媒体资源未渲染出来之前,应当在页面中使用相同尺寸的占位图像

如果不使用占位符,图像延迟显示出来后,尺寸更改可能会使页面布局出现移位

这种现象不仅会对用户体验带来困惑,更严重的还会触发浏览器成本高昂的回流机制,进而增加系统资源开销造成卡顿

而用来占位的图像解决方案也有多种,十分简单的方式是使用一个与目标媒体资源长宽相同的纯色占位符,或者像之前使用的Base64图片,当然也可以采用LQIP或SQIP等方法。

其中LQIP的全称是低质量图片占位符,即使用原图的较低分辨率版本来占位,SQIP则是一种基于SVG的LIQP技术,我们可以通过对比来感知它们和原图之间的差别,如图所示:

image-20220221150819915

其实就是以最小的带宽消耗,告知用户此处将要展示一个媒体资源,可能由于资源尺寸较大还在加载

对于使用〈img〉标记的图像资源,应将用于占位的初始图像指给src属性,直到更新为所需的最终图像为止

而对于使用〈video〉标记的视频资源,则应将占位图像指给poster属性,除此之外,最好可以在〈img〉和〈video〉标签上添加表示宽width和高height的属性,如此便可确保不会在占位符转化为最终媒体资源时,发生元素渲染大小的改变

内容加载失败

在进行延迟加载过程中,可能会因为某种原因而造成媒体资源加载失败,进而导致错误的情况

比如用户访问某个网站后,保持浏览器该选项卡打开后长时间离开,等再返回继续浏览网页内容时,可能在此过程中网站已经进行了重新部署,原先访问的页面中包含的部分媒体资源由于哈希的版本控制发生更改,或者已被移除。那么用户滚动浏览页面,遇到延迟加载的媒体资源,可能就已经不可使用了。

虽然类似情况发生的概率不高,但考虑网站对用户的可用性,开发者也应当考虑好后备方案,以防止类似延迟加载可能遇到的失败。例如,图像资源可以采取如下方案进行规避:

image-20220221150956955

当图片资源未能按预期成功加载时,所采取的具体处理措施应当依据应用场景而定

比如,当请求的媒体资源无法加载时,可将使用的图像占位符替换为按钮,让用户单击以尝试重新加载所需的媒体资源,或者在占位符区域显示错误的提示信息

总之,在发生任何资源加载故障时,给予用户必要的通知提示,总好过直接让用户无奈地面对故障

图像解码延迟

在前面章节介绍 JPEG 图像的编解码时,我们知道渐进式的JPEG会先呈现出一个低像素的图像版本,随后会慢慢呈现出原图的样貌。这是因为图像从被浏览器请求获取,再到最终完整呈现在屏幕上,需要经历一个解码的过程,图像的尺寸越大,所需要的解码时间就越长。如果在 JavaScript 中请求加载较大的图像文件,并把它直接放入 DOM 结构中后,那么将有可能占用浏览器的主进程,进而导致解码期间用户界面出现短暂的无响应。

为减少此类卡顿现象,可以采用decode方法进行异步图像解码后,再将其插入 DOM 结构中。但目前这种方式在跨浏览器场景下并不通用,同时也会复杂化原本对于媒体资源延迟加载的处理逻辑,所以在使用中应进行必要的可用性检查。下面是一个使用 Image.decode() 函数来实现异步解码的示例:

image-20220221151120094

对应的JavaScript事件处理代码如下:

image-20220221151205281 image-20220221151247019

需要说明的是,如果网站所包含的大部分图像尺寸都很小,那么使用这种方式的帮助并不会很大,同时还会增加代码的复杂性。但可以肯定的是这么做会减少延迟加载大型图像文件所带来的卡顿

JavaScript 是否可用

在通常情况下,我们都会假定JavaScript始终可用,但在一些异常不可用的情况下,开发者应当做好适配,不能始终在延迟加载的图像位置上展示占位符。可以考虑使用〈noscript〉标记,在JavaScript不可用时提供图像的真实展示:

image-20220221151340794

如果上述代码同时存在,当JavaScript不可用时,页面中会同时展示图像占位符和〈noscript〉中包含的图像,为此我们可以给〈html〉标签添加一个no-js类:

image-20220221151411866

在由〈link〉标签请求CSS文件之前,在〈head〉标签结构中放置一段内联脚本,当JavaScript可用时,用于移除no-js类:

image-20220221151449717

以及添加必要的CSS样式,使得在JavaScript不可用时屏蔽包含.lazy类元素的显示:

image-20220221151525054

当然这样并不会阻止占位符图像的加载,只是让占位符图像在JavaScript不可用时不可见,但其体验效果会比让用户只看到占位符图像和没有意义的图像内容要好许多

2.资源优先级

浏览器向网络请求到的所有数据,并非每个字节都具有相同的优先级或重要性

所以浏览器通常都会采取启发式算法,对所要加载的内容先进行推测,将相对重要的信息优先呈现给用户,比如浏览器一般会先加载CSS文件,然后再去加载JavaScript脚本和图像文件。

但即便如此,也无法保证启发式算法在任何情况下都是准确有效的,可能会因为获取的信息不完备,而做出错误的判断

本节就来探讨如何影响浏览器对资源加载的优先级

优先级

浏览器基于自身的启发式算法,会对资源的重要性进行判断来划分优先级,通常从低到高分为:Lowest、Low、High、Highest等

比如,在 <head> 标签中,CSS 文件通常具有最高的优先级 Highest,其次是 <script> 标签所请求的脚本文件,但当 <script> 标签带有 defer 或 async 的异步属性时,其优先级又会降为 Low

我们可以通过 Chrome 的开发者工具,在 Network 页签下找到浏览器对资源进行的优先级划分,如下图所示:

image-20220221164334474

我们可以通过该工具,去了解浏览器为不同资源分配的优先级情况,细微的差别都可能导致类似的资源具有不同的优先级,比如首屏渲染中图像的优先级会高于屏幕视窗外的图像的优先级

本书不会详细探讨 Chrome 如何为当前资源分配优先级,读者如有兴趣可通过搜索“浏览器加载优先级”等关键字自行了解。本书对性能优化实战而言,会更加关注:当发现资源默认被分配的优先级不是我们想要的情况时,该如何更改优先级。

接下来介绍三种不同的解决方案:首先是前面章节提到过的预加载,当资源对用户来说至关重要却又被分配了过低的优先级时,就可以尝试让其进行预加载或预连接;如果仅需要浏览器处理完一些任务后,再去提取某些资源,可尝试使用预提取

预加载

使用 <link rel="preload"> 标签告诉浏览器当前所指定的资源,应该拥有更高的优先级,例如:

<link rel="preload" as="style" href="a.css">
<link rel="preload" as="script" href="b.js">

这里通过as属性告知浏览器所要加载的资源类型,该属性值所指定的资源类型应当与要加载的资源相匹配,否则浏览器是不会预加载该资源的

需要注意的是,<link rel="preload"> 会强制浏览器进行预加载,它与其他对资源的提示不同,浏览器对此是必须执行而非可选的。因此,在使用时应尽量仔细测试,以确保使用该指令时不会提取不需要的内容或重复提取内容

如果预加载指定的资源在 3s 内未被当前页面使用,则浏览器会在开发者工具的控制台中进行警告提示,该警告务必要处理,如下图所示:

image-20220221165240492

接下来看两个使用实例:字体的使用和关键路径渲染

通常字体文件都位于页面加载的若干 CSS 文件的末尾,但考虑为了减少用户等待文本内容的加载时间,以及避免系统字体与偏好字体发生冲突,就必须提前获取字体。因此我们可以使用 <link rel="preload"> 来让浏览器立即获取所需的字体文件:

image-20220221165349239

这里的crossorigin属性非常重要,如果缺失该属性,浏览器将不会对指定的字体进行预加载

在讲页面渲染生命周期时,提到过关键渲染路径,其中涉及首次渲染之前必须加载的资源(比如CSS和JavaScript等),这些资源对首屏页面渲染来说是非常重要的。以前通常建议的做法是把这些资源内联到HTML中,但对服务器端渲染或对页面而言,这样做很容易导致带宽浪费,而且若代码更改使内联页面无效,无疑会增加版本控制的难度

所以使用 <link rel="preload"> 对单个文件进行预加载,除了能很快地请求资源,还能尽量利用缓存

其唯一的缺点是可能会在浏览器和服务器之间发生额外的往返请求,因为浏览器需要加载解析 HTML 后,才会知道后续的资源请求情况

其解决方式可以利用 HTTP 2 的推送,即在发送 HTML 的相同连接请求上附加一些资源请求,如此便可取消浏览器解析HTML到开始下载资源之间的间歇时间

但对于 HTTP 2 推送的使用需要谨慎,因为控制了带宽使用量,留给浏览器自我决策的空间便会很小,可能不会检索已经缓存了的资源文件

预连接

通常在速度较慢的网络环境中建立连接会非常耗时,如果建立安全连接将更加耗时

其原因是整个过程会涉及 DNS 查询、重定向和与目标服务器之间建立连接的多次握手,所以若能提前完成上述这些功能,则会给用户带来更加流畅的浏览体验,同时由于建立连接的大部分时间消耗是等待而非数据交换,这样也能有效地优化带宽的使用情况。解决方案就是所谓的预连接:

image-20220221165525749

通过 <link rel="preconnect"> 标签指令,告知浏览器当前页面将与站点建立连接,希望尽快启动该过程

虽然这么做的成本较低,但会消耗宝贵的 CPU 时间,特别是在建立 HTTPS 安全连接时。如果建立好连接后的 10s 内,未能及时使用连接,那么浏览器关闭该连接后,之前为建立连接所消耗的资源就相当于完全被浪费掉了

另外,还有一种与预连接相关的类型 <link rel="dns-prefetch">,也就是常说的 DNS 预解析,它仅用来处理 DNS 查询,但由于其受到浏览器的广泛支持,且缩短了 DNS 的查询时间的效果显著,所以使用场景十分普遍

预解析 DNS

预提取

前面介绍的预加载和预连接,都是试图使所需的关键资源或关键操作更快地获取或发生,这里介绍的预提取,则是利用机会让某些非关键操作能够更早发生

这个过程的实现方式是根据用户已发生的行为来判断其接下来的预期行为,告知浏览器稍后可能需要的某些资源。也就是在当前页面加载完成后,且在带宽可用的情况下,这些资源将以 Lowest 的优先级进行提起

显而易见,预提取最适合的场景是为用户下一步可能进行的操作做好必要的准备,如在电商平台的搜索框中查询某商品,可预提取查询结果列表中的首个商品详情页;或者使用搜索查询时,预提取查询结果的分页内容的下一页:

image-20220221173250585

需要注意的是,预提取不能递归使用,比如在搜索查询的首页 page-1.html 时,可以预提取当前页面的下一页 page-2.html 的 HTML 内容,但对其中所包含的任何额外资源不会提前下载,除非有额外明确指定的预提取

另外,预提取不会降低现有资源的优先级,比如在如下 HTML 中:

image-20220221173337760

可能你会觉得对 style.css 的预提取声明,会降低接下来 <link rel="stylesheet"href="style.css"> 的优先级,但其真实的情况是,该文件会被提取两次,第二次可能会使用缓存,如下图所示:

image-20220221173414721

显然两次提取对用户体验来说非常糟糕,因为这样不但需要等待阻塞渲染的 CSS,而且如果第二次提取没有命中缓存,必然会产生带宽的浪费,所以在使用时应充分考虑

3.小结

本章主要探讨了页面涉及资源的加载性能的优化内容,首先介绍了在图像文件中常见的多种延迟加载方案和实现细节,以及在较新版本的Chrome浏览器中,原生支持的延迟加载方案和使用过程中需考虑的兼容性处理

以及笔者总结在实践过程中有关延迟加载可能会忽略的注意事项

最后通过介绍浏览器对资源加载优先级的划分,引出了三种更改优先级的解决方案:预加载、预连接和预提取。总而言之,对于加载方面的优化原则可以概括为两点:尽快呈现给用户尽可能少的必备资源;充分利用系统或带宽的空闲时机,来提前完成用户稍后可能会进行的操作过程或加载将要请求的资源文件