跳过正文
  1. 前端工程体验优化实战/

万物皆可懒加载:3类通用资源懒加载实现方案

hujiacheng
作者
hujiacheng
Front-end Developer / Strive To Become Better
目录
图片与懒加载 - 这篇文章属于一个选集。
§ 3: 本文

最好的代码,就是没有代码。

同样的,体验最好的网页,就是没有内容的网页。

这段话乍一听似乎脱离实际,没有内容的网页根本没有加载的必要。但仔细分析,却道出了前端体验优化的核心:精简页面内容

再多优化,都比不上从根源上精简页面的内容,减少加载的资源。所以本节我们将一起学习能从根本上为前端工程精简加载资源体积和资源数量的解决方案:懒加载各类资源

在第6节,我们已经学习了《使用动态引用语法import()实现代码模块懒加载》的方案,这一节我们则主要关注资源懒加载

资源懒加载一般应用于需要加载外部资源的元素,例如<img>, <video>, <iframe>, <picture>等,当懒加载目标元素在视口(ViewPort)时,不加载对应资源。目标元素接近或进入视口时,触发加载资源。

从而减少页面加载资源的数量,精简加载资源体积,优化用户体验。

1. 3类资源懒加载实现方案
#

下面我们将以<img>元素为例,来一一学习的3类懒加载解决方案的核心原理及具体实现,它们分别是:

  • 监听滚动事件方案
  • 浏览器 Intersection Observer API方案
  • 浏览器原生懒加载方案

1. 监听滚动事件方案
#

第一种实现方案是通过监听浏览器页面滚动事件(scroll),动态计算视口内元素位置,从而判断懒加载目标元素,触发资源加载。

以图片元素懒加载为例,我们需要监听页面滚动事件,并计算出每张图片与视口顶部的相对距离。如果图片在视口内,就加载该图片,否则就不加载。

请看代码示例:

// 1. 获取所有需要懒加载的图片元素
const lazyloadImages = document.querySelectorAll('[data-lazyload]');

// 2. 监听页面滚动事件
window.addEventListener('scroll', () => {
  lazyloadImages.forEach(img => {
    if (img.getAttribute('data-loaded')) {
      return;
    }
    // 3.  获取图片与视口顶部的相对距离
    const topPos = img.getBoundingClientRect().top;
    // 4. 与 视口高度(window.innerHeight)对比,判断是否在视口内
    if (topPos < window.innerHeight) {
      // 5. 如果图片在当前视口内,就加载该图片
      img.src = img.getAttribute('data-src');
      img.setAttribute('data-loaded', true)
    }
  });
});

对应的HTML代码是:

<img data-lazyload data-src="/example-image.jpg" />

在上述代码中,我们首先获取所有需要懒加载的图片元素,通过querySelectorAll 获取带有指定的懒加载标记属性data-lazyload的元素引用,这些图片初始化时没有设置src值,而真正的懒加载目标图片URL则保存在 data-src 属性中,这样就能实现初始化时暂不加载图片,由代码逻辑控制图片何时加载。

然后,我们监听页面滚动事件window.addEventListener('scroll'),在滚动事件的回调中,遍历所有需要懒加载的图片元素lazyloadImages.forEach

对于每个图片元素,我们首先检查其 data-loaded 属性是否为 true,如果是,则说明该图片已经加载过了,我们就跳过该元素。

否则,我们通过getBoundingClientRect()方法获取该图片距离视口(viewport)顶部的位置,并通过和当前视口高度(window.innerHeight)对比,判断该图片当前是否在视口内,即图片与视口顶部的距离,是否小于当前视口的高度。

最后,如果图片在视口内,就将该图片的 src 属性设置为 data-src 属性的值,从而触发图片资源加载,并将 data-loaded 属性设置为 true,表示该图片已经加载过了。

这样我们就实现了对图片类资源的懒加载,初始化时暂不加载图片资源,减少加载资源数量和体积,提高初始化时渲染性能,当图片进入浏览器视口时,再真正触发加载。

实际应用时,我们应该对这一方案做更多细节上的优化,如:

  • 设置图片默认的宽高,以避免懒加载完成后,页面高度变化,被判断为意外布局变化,影响CLS评分。
  • 添加图片未加载占位符和加载中动画,改善用户等待加载时的视觉和体验。
  • 计算位置、判断目标元素是否出现在视口时,还要考虑水平方向上的页面滚动位置,计算getBoundingClientRect()返回的top, bottom, left, right4个方向相对视口位置。
  • scroll事件添加节流优化,降低触发回调函数的频率,避免影响页面渲染的FPS。

依赖scroll事件,容易对页面渲染性能产生负面影响,这也是监听滚动事件方案的主要痛点

过低的scroll事件回调触发频率,会导致懒加载触发不灵敏。

而过高的scroll事件回调触发频率,又会因为大量计算,导致JS执行耗时太长,阻塞UI绘制,产生页面卡顿。

为了解决这些痛点,浏览器平台的 Intersection Observer API应运而生。

2. Intersection Observer API 监听元素距视口位置方案
#

第二类懒加载方案是使用 Intersection Observer API,该API可用于监听元素距离视口(Viewport)位置,当元素进入视口时,触发指定回调函数,从而实现懒加载、无限滚动等功能,

使用该API可以省略编写代码计算元素相对位置的逻辑,解决JS监听滚动事件实现懒加载方案需要频繁计算元素位置的痛点,有助于提高懒加载的性能表现和用户体验。

请看代码示例:

// 创建 IntersectionObserver 实例
const observer = new IntersectionObserver(entries => {
  // 遍历所有观察的元素
  entries.forEach(entry => {
    // 如果该元素进入了视口,就加载该元素的图片
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

const lazyImages = document.querySelectorAll('img.lazy');

// 开始监听所有需要懒加载的图片元素
lazyImages.forEach(img => {
  observer.observe(img);
});

在上述代码中,我们首先创建了一个IntersectionObserver实例observer,该实例可用于监视多个元素是否进入视口。

然后,我们获取所有需要懒加载的图片元素,并分别调用 observe() 方法,开始监视每个图片元素。

当某一个图片元素进入了视口时,创建实例时传入的第一个参数——回调函数就会被触发。

进入视口的每一个图片元素,会生成一条重叠记录数据(IntersectionObserverEntry ) ,成为回调函数第一个参数entries中的一个元素。

重叠记录数据(IntersectionObserverEntry ) 主要包含以下数据:

  • target:对象值,监视目标元素的引用
  • isIntersecting:布尔值,表示目标元素是否和视口重叠,即是否进入了视口。

重叠记录数据示例

{
    boundingClientRect: DOMRectReadOnly {x: 107.1875, y: -172.8125, width: 300.015625, height: 500, top: -172.8125, }
    intersectionRatio: 0.4854249954223633
    intersectionRect: DOMRectReadOnly {x: 107.1875, y: 84.46875, width: 300.015625, height: 242.71875, top: 84.46875, }
    isIntersecting: false
    isVisible: false
    rootBounds: DOMRectReadOnly {x: 8, y: 84.46875, width: 483.203125, height: 583.21875, top: 84.46875, }
    target: div.box
    time: 839005.1999998093
}

在回调函数中,我们可以遍历所有重叠记录数据,如果某一条重叠记录数据isIntersecting 属性为 true,就表示这条记录对应的元素进入了视口,可以通过target属性获取元素引用。

我们就可以将该元素的src属性设置为 data-src 属性的值,触发src对应图片资源加载,实现懒加载的逻辑。

最后,因为这个元素已经触发过了加载,我们还可以使用unobserve()方法停止观察该元素,避免重复触发回调函数,同时节省内存资源。

拓展知识:IntersectionObserver,不仅可以监视元素视口的重叠关系,还可以在初始化实例时,传入第二个参数options

  • 指定root,值为监视目标元素父元素的引用,从而监视任意父子元素的重叠关系。但前提是父元素必须是可以滚动的,例如设置了CSS样式:overflow: scroll;

  • 指定threshold,传入[0, 1.0]区间内的数据,指定重叠判定的比例。

    • 值为0表示子元素有1个像素,出现在父元素视口内,就触发回调。
    • 值为1表示子元素全部出现在父元素视口内,才触发回调。

通过这些细节选项,可以进一步实现无限滚动、判断元素是否可见等复杂逻辑。

注:Intersection Observer API 兼容性情况如下:

  • Chrome 版本 51+ 支持(发布于2016年)
  • Edge 版本 15+ 支持(发布于2017年)
  • Safari 版本 12.1+ 支持(发布于2019年)

Intersection Observer兼容性详细数据:https://caniuse.com/intersectionobserver

通过上述介绍,相信大家也有所体会,Intersection Observer API虽然功能强大,但仍然有一定的学习和使用成本。

为了解决成本较高的痛点,使用成本几乎为0的浏览器原生懒加载方案近年来顺势而生。

3. 14个字符实现懒加载:浏览器原生loading="lazy"属性
#

第三种懒加载方案是使用浏览器原生loading="lazy"属性,这个属性目前可以用于<img><iframe>2类元素。

只要元素添加了lazyload属性,浏览器就会为我们处理各种细节,实现懒加载效果,无需任何额外代码逻辑,使用成本非常低,开发体验极佳!

请看代码示例:

<img loading="lazy" src="example.jpg" alt="Example Image" />

<iframe loading="lazy" src="https://github.com/JuniorTour"></iframe>

上述代码示例中,和没有懒加载的<img><iframe>相比,唯一的区别就是多了loading="lazy"属性,其余的srcalt等属性没有任何改变,也不需要新增额外代码逻辑,就能实现目标元素在视口外不加载资源,进入视口时才触发加载资源的懒加载效果。

上面我们介绍的2种方案,都需要配套上百行JS代码逻辑,才能实现懒加载,但loading="lazy"属性完全不需要额外代码逻辑,对比之下,美好的像是一场梦。

可惜美梦终有醒来的时候,loading="lazy"属性的缺点也非常显著,主要体现在以下2方面:

1. 浏览器兼容性一般
#

loading="lazy"属性浏览器兼容性如下:

  • Chrome 77 +(2019年发布)
  • Edge 79 +(2020年发布)
  • Safari 15.4 +(2022年发布)
  • Chrome for Android 119+ (2023年发布)

兼容性详细数据:https://caniuse.com/loading-lazy-attr

可以看出,这一属性目前只能在近3年推出的浏览器上使用,在移动端更是到了2023年才得以被支持。在生产环境全面使用loading="lazy"属性,目前还只能覆盖部分用户。

2. 支持的元素太少
#

此外,loading="lazy"属性目前只能应用在<img><iframe>2类元素上,其他需要加载资源的元素,例如:<video><picture>、CSS background-image原生懒加载属性都还没有支持。

即使各大浏览器平台现在开始实现对更多元素懒加载的支持,考虑到用户使用浏览器版本更新滞后的现实情况,想要全面应用到生产环境,也还是免不了要再等待数年。

那么,在2024年我们想要在生产环境全面应用资源懒加载优化,应该如何选择呢?

一个可行的方案是,基于各类懒加载实现方案,封装一套工具库。

笔者已经基于工作经验,封装了一套通用懒加载库,致力于解决资源懒加载兼容性使用成本高这2大痛点,下面我们就一起学习一下这个懒加载库的实现原理和使用方式。

图片与懒加载 - 这篇文章属于一个选集。
§ 3: 本文

相关文章