前端性能优化:4.资源加载优化

根据HTTP Archive网站的统计数据,截止到2021年12月互联网页面的请求数据量中,图像资源已经占据了60%~65%,这仅是平均数据,对于电商或图片社交场景,这个比例将更大。
想要获得更好的性能体验,只靠资源压缩与恰当的文件格式类型,是很难满足期望的。我们还需要针对资源加载过程进行优化,该环节所要做的内容可概括为分清资源加载的优先级顺序,仅加载当前所必需的资源,并利用系统空闲提前加载可能会用到的资源。即本文将探讨的内容:资源的优先级、延迟加载和预加载。

4.1 图像延迟加载

4.1.1什么是延迟加载
延迟加载就是通过延迟加载非关键的图片及视频资源,使得页面内容更快地呈现在用户面前。
例如在首次打开网站时,应尽可能只加载首屏内容所包含的资源,而首屏之外涉及的图片或视频,可以等到用户滚动视窗浏览时再去加载。

4.1.2 实现图片的延迟加载:传统方式
通过监听scroll事件与resize事件,并在事件的回调函数中去判断,需要进行延迟加载的图片是否进入视窗区域。
img标签结构:

<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load-1x.jpg">

只需关注3个属性:

  1. class属性,稍后会在JavaScript中使用类选择器选取需要延迟加载处理的img标签。
  2. src属性,加载前的占位符图片,可以用Base64图片或低分辨率的图片。
  3. data-src属性,通过该自定义属性保存图片真实的URL外链。

具体的JavaScript实现逻辑如下,在文档的DOMContentLoaded事件中,添加延迟加载处理逻辑,首先获取class属性名为lazy的所有<img>标签,将这些标签暂存在一个名为lazyImages的数组中,表示需要进行延迟加载但还未加载的图片集合。当一个图片被加载后,便将其从lazyImages数组中移除,知道lazyImages数组为空时,表示所有待延迟加载的图片均已加载完成,此时便可将页面滚动事件移除。
关键是判断图片是否出现在视窗中,可以使用getBoundingClientRect()函数获取元素的相对位置。

// 传统方式实现懒加载
document.addEventListener("DOMContentLoaded", function() {
    // 获取所有需要懒加载的图片
    let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
    // 限制函数频繁被调用
    let active = false;
    const lazyLoad = function() {
        if (active === false) {
            active = true;
            setTimeout(function() {
                lazyImages.forEach(function(lazyImage) {
                    // 判断图片是否出现在视窗中
                    if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
                        // 将真实的图片URL赋值给src属性,发起请求加载资源
                        lazyImage.src = lazyImage.dataset.src;
                        // 图片加载完成后,取消监控以防止重复加载
                        lazyImage.classList.remove("lazy");
                        lazyImages = lazyImages.filter(function(image) {
                            return image !== lazyImage;
                        });
                        // 所有懒加载图片加载完成后,移除事件触发处理函数
                        if (lazyImages.length === 0) {
                            document.removeEventListener("scroll", lazyLoad);
                            window.removeEventListener("resize", lazyLoad);
                            window.removeEventListener("orientationchange", lazyLoad);
                        }
                    }
                });
                active = false;
            }, 200);
        }
    };
    document.addEventListener("scroll", lazyLoad);
    window.addEventListener("resize", lazyLoad);
    window.addEventListener("orientationchange", lazyLoad);
});

4.1.3 实现图片的延迟加载:Intersection Observer 方式
现代浏览器大多都已经支持了Intersection Observer API,可以通过它来检查目标元素的可见性,这种方式的性能和效率都比较好。
Intersection Observer概念简述:每当因页面滚动或窗口尺寸发生变化,使得目标元素(target)与设备视窗或其他指定元素产生交集时,便会触发通过Intersection Observer API配置的回调函数,在该回调函数中进行延迟加载的逻辑处理,会比传统方式显得更加简洁而高效。
将这种方式引入项目之前,应当确保已做到以下两点:

  1. 做好尽量晚辈浏览器兼容性检查,对于兼容Intersection Observer API的浏览器,采用这种方式处理,对于不兼容的浏览器,则切换回传统的实现方式进行处理。
  2. 使用相应兼容的polyfill插件。
// Intersection Observer方式实现懒加载
document.addEventListener("DOMContentLoaded", function() {
    var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));;
    // 判断浏览器兼容性
    if ("IntersectionObserver" in window && "IntersectionObserverEntry" in window && "intersectionRatio" in window.IntersectionObserverEntry.prototype) {
        // 新建IntersectionObserver对象,并在其回调函数中实现关键加载逻辑
        let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
            entries.forEach(function(entry) {
                // 判断图片是否出现在视窗中
                if (entry.isIntersecting) {
                    let lazyImage = entry.target;
                    lazyImage.src = lazyImage.dataset.src;
                    // 图片加载完成后,取消监控以防止重复加载
                    lazyImage.classList.remove("lazy");
                    lazyImageObserver.unobserve(lazyImage);
                }
            });
        });
        lazyImages.forEach(function(lazyImage) {
            lazyImageObserver.observe(lazyImage);
        });
    }
});

4.1.4 实现图片的延迟加载:CSS类名方式
这种实现方式通过CSS的background-image属性来加载图片,与判断<img>标签src属性是否有要请求图片的URL不同,CSS中图片的加载行为建立在浏览器对文档分析基础之上。
当DOM树、CSSOM树及渲染树生成后,浏览器回去检查CSS以何种方式应用于文档,再决定是否请求外部资源。如果浏览器确定涉及外部资源请求的CSS规则在当前文档中不存在时,便不会去请求该资源。
具体实现方式是通过JavaScript来判断元素是否出现在视窗中的,当在视窗中时,为其元素的class属性添加visible类名。而在CSS文件中,为同一类名元素定义出带.visible和不带.visible的两种包含background-image规则。

4.1.5 原生的延迟加载支持
从Chrome75版本开始,已经可以通过<img><iframe>标签的loading属性原生支持延迟加载了。
loading属性包含以下三种取值:

  1. lazy:进行延迟加载。
  2. eager:立即加载。
  3. auto:浏览器自行决定是否进行延迟加载。

注意:若不指定任何属性值,loading默认值为auto。
之前讲到延迟触发的时机,都是当目标图像文件经页面滚动出现在屏幕视窗中时,触发对图像资源的请求。但从体验上考虑,这样处理并不完美,因为当图像标签出现在屏幕视窗中时,还只是占位符图像。
若网络存在延迟或图像资源过大,那么它的请求加载过程是可以被用户感知的。更好的做法是在图像即将滚动出现在屏幕视窗之前的一段距离,就开始请求加载图像或iframe中的内容。
兼容性处理:通过使用新技术优化了延迟加载的实现方式,同时也应当注意新技术在不同浏览器之间的兼容性。

4.2 视频加载

与延迟加载图像资源类似,通过<video>引入的视频资源也可进行延迟加载,但通常都会根据需求场景进行具体的处理。

4.2.1 不需要自动播放
由于Chrome等一些浏览器会对视频资源进行预加载,即在HTML完成加载和解析时触发DOMContentLoaded事件开始请求视频资源,当请求完成后触发window.onload事件开始页面渲染。
为了使页面更快地加载并渲染出来,可以阻止不需要自动播放的视频的预加载,其方法是通过视频标签的preload进行控制:

<video controls preload="none" poster="default.jpg">
  <source src="simply.webm" type="video/webm">
</video>

<video>标签的preload属性通常的默认值为auto,表示无论用户是否希望,所有视频文件都会被自动下载,这里将其设置为none,来阻止视频的自动预加载。poster属性为视频提供占位符图片。考虑类似边缘异常场景是必要的,因为浏览器对视频的加载行为可能存在较大差别。

  1. Chrome之前的版本中,preload的默认值是auto,从64版本以后其默认值改为了metadata,表示仅加载视频的元数据,Firefox、IE11和Edge等浏览器的行为类似。
  2. Safari 11.0的Mac版会默认进行部分视频资源预加载,11.2的Mac版后仅可预加载元数据,但iOS的Safari不会对视频预加载。
  3. 若浏览器开启了节省流量模式后,preload将默认设置为none。、

若站点中包含了同一域名下的多个视频资源,那么推荐最好将preload的属性设置为metadata,或者定义poster属性值时讲preload设置为none,这样能很好地避免HTTP最大连接数的限制,因为通常HTTP1.1协议规定统一域名下的最大连接数为6,超出后多余的连接会被挂起,无疑会对性能造成负面影响。

4.2.2 视频代替GIF动画
应当尽量用视频代替尺寸过大的GIF动画。
虽然GIF动画的应用历史和范围都很广泛,但其在输出文件大小、图像色彩质量等许多方面的表现均不如视频。

4.3 加载注意事项

4.3.1 首屏加载
了解过延迟加载的诸多优点之后,读者是否有使用JavaScript对页面上所有的内容都进行延迟加载的冲动?在采取优化措施前需要注意的是,对于优化工作来说,不存在一蹴而就的解决方案,而是需要根据具体场景采用恰当的方式。
比如对于首屏上的内容就不应该延迟加载,因为延迟加载会将图像或视频等媒体资源延迟到DOM可交互之后,即脚本完成加载并开始执行时才会进行。所以对于首屏视窗之外的资源采用延迟加载,而对首屏之内的资源采用正常方式加载,会带来更好的性能体验。
由于网站页面呈现的设备屏幕尺寸多种多样,因此如何判断首屏视窗的边界,就会因设备不同而有所不同。读者需要自行把握屏幕自适应。
此外,将视窗边界作为延迟加载触发的阈值,其实并非最佳的性能考虑。更理想的做法是,在延迟加载的媒体资源到达首屏边界之前设置一个缓冲区,以便媒体资源在进入视窗之前就开始进行加载。

4.3.2 资源占位
当延迟加载的媒体资源未渲染出来之前,应当在页面中使用相同尺寸的占位图像。如果不使用占位符,图像延迟显示出来后,尺寸更改可能会使页面布局出现移位。
用来占位的图像解决方案也有很多种,十分简单的方式是用一个与目标媒体资源长款相同的纯色占位符,或者像之前使用的Base64图片,当然也可以采用LQIP(低质量图片)或SQIP(基于SVG的LQIP)等方法。
目标是以最小的带宽消耗,告知用户此处将要展示一个媒体资源,可能由于资源尺寸比较大还在加载。

4.3.3 内容加载失败
延迟加载过程中,可能会因为某种原因而造成媒体资源加载失败,进而导致错误的情况。虽然类似情况发生的概率不高,但考虑网站对用户的可用性,开发者也应考虑好后备方案,以防止类似延迟加载可能遇到的失败。

4.3.4 图像解码延迟
图像从被浏览器请求获取,再到最终完整呈现在屏幕上,需要经历一个解码过程,图像尺寸越大,所需要的解码时间就越长。如果在JavaScript中请求加载较大的图像文件,并把它直接放入DOM结构中后,那么将有可能占用浏览器的主进程,进而导致解码期间用户捷尔缅出现短暂的无响应。
为减少此类卡顿现象,可以采用decode方法进行异步图像解码后,再将其插入DOM结构中。
注意:若网站包含的大部分图像尺寸都很小,那么此方法帮助并不会很大。

4.3.5 JavaScript是否可用
部分异常情况下JavaScript并不是始终可用的,开发者应当做好适配,不能始终在延迟加载的图像位置上展示占位符。可以考虑使用noscript标记:

<!- 使用延迟加载的图像文件标签 -->
<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load.jpg" alt="I'm an image!">
<!- 当JavaScript不可用时 -->
<noscript>
    <img src="image-to-lazy-load.jpg" alt="I'm an image!">
</noscript>

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

<html class="no-js">

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

<script>document.documentElement.classList.remove("no-js")</script>

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

.no-js .lazy{
    display:none;
}

这样并不会阻止占位符图像加载,只是让占位符图像在JavaScript不可用时不可见。

4.4 资源优先级

浏览器向网络请求道的所有数据,并非每个字节都具有相同的优先级或重要性。所以浏览器通常都会采取启发式算法,对所要加载的内容先进行推测,将相对重要的信息优先呈现给用户,比如浏览器一般会先加载CSS文件,然后再去加载JavaScript和图像。

4.4.1 优先级
浏览器基于自身的启发式算法,会对资源的重要性进行判断来划分优先级,通常从低到高分为:Lowest、Low、High、Highest等。
比如,在<head>标签中,CSS文件通常具有最高的优先级Highest,其次是<script>标签请求的脚本文件,但当<script>标签带有defer或async的异步属性时,其优先级又会将为Low。可以通过Chrome的开发者工具,在network页签下找到浏览器对资源进行的优先级划分。
在这里插入图片描述

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

<link rel="preload" as="script" href="important.js">
<link rel="preload" as="style" href="critical.css">

这里通过as属性告知浏览器索要加载的资源类型,该属性值所指定的资源类型应与要加载的资源相匹配,否则浏览器是不会预加载该资源的。
注意:<link rel="preload">会强制浏览器进行预加载,而非可选的。因此,使用时应尽可能仔细测试,以确保使用时不会提取不需要的内容或重复提取内容。
使用<link rel="preload">对单个文件斤西瓜预加载,除了能很快地请求资源,还能尽量利用缓存。其唯一的缺点时是可能会在浏览器和服务器之间发生额外的往返请求,因为浏览器需要加载解析HTML后,才会知道后续的资源请求情况。其解决方式可以利用HTTP2的推送,即在发送HTML的相同练级请求上附加一些资源请求,如此便可取消浏览器解析HTML到开始下载资源之间的间歇时间。但对于HTTP2推送的使用需要谨慎,因为控制了带宽使用量,留给浏览器自我决策的空间便会很小,可能不会检索已经缓存了的资源文件。

4.4.3 预连接
通常在速度较慢的网络环境中建立连接会非常耗时,如果建立安全连接将更加耗时。其原因是整个过程会涉及DNS查询、重定向和与目标服务器之间建立连接的多次握手,所以若能提前完成上述功能,则会给用户带来更加流畅的浏览体验,同时由于建立连接的大部分时间消耗是等待而而非数据交换,这样也能有效地优化带宽的使用情况。

<link rel="preconnect" href="https://example.com">

通过<link rel="preconnect">标签指令,告诉浏览器当前页面将与站点建立连接。虽然这么做的成本较低,但会消耗宝贵的CPU时间,特别是在建立HTTPS安全连接时。
另外还有一种与预连接相关的类型<link rel="dns-prefetch">,也就是常说的DNS预解析,仅用来处理DNS查询,但由于其受到的浏览器的广泛支持,且缩短了DNS的查询时间的效果显著,所以使用场景十分普遍。

4.4.4 预提取
前面介绍的预加载喝预连接,都是试图时所需的关键资源或关键操作更快地获取或发生,这里介绍的预提取,则是利用机会让某些非关键操作能够更早发生。
这个过程的实现方式是根据用户已发生的行为来判断其接下来的预期行为,告知浏览器稍后可能需要的某些资源。也就是在当前页面加载完成后,且在带宽可用的情况下,这些资源将以Lowest的优先级进行提起。
显而易见,预提取最适合的场景是为用户下一步可能进行的操作做好必要的准备,如在电商平台的搜索框中查询某商品,可预提取查询结果列表中的首个商品详情页;或者使用搜索查询时,与提取查询结果的分页内容的下一页:

<link rel="prefetch" href="page-2.html">

注意:预提取不能递归使用。

4.5 小结

  1. 尽快呈现给用户尽可能少的必备资源。
  2. 充分利用系统或带宽的空闲时间,来提前完成用户稍后可能会进行的草错过程或加载将要请求的资源文件。

前端性能优化系列:
前端性能优化:1.什么是前端性能优化
前端性能优化:2.前端页面的生命周期
前端性能优化:3.图像资源优化
前端性能优化:4.资源加载优化
前端性能优化:5.高性能的JavaScript代码
前端性能优化:6.项目构建优化
前端性能优化:7.页面渲染优化

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值