媒体资源懒加载


懒加载常识

页面中可能会出现很多媒体资源,比如图片列表,视频列表等等。加载首屏看不到的图片只会浪费带宽,而且影响首屏打开速度。

上述情况也可以通过分页控制,一次只加载少量资源,通过手动会滚动到底部进行翻页,批量加载,这是另一种处理方式,这种方式的确更合理,但是 SEO 就不够理想。

懒加载的方式,就是仅加载当前视口内的媒体资源,用户拉动滚动条,当需要展示的媒体资源进入页面后,再进行加载。

懒加载的设计方式就是监听相关媒体元素进入可见范围后,再对其资源进行加载。

滚动监听实现懒加载 兼容方案

通过滚动监听实现懒加载的方法很简单且兼容性高,但是代码成本高,且有一些缺陷。

首先懒加载的媒体资源不应该直接挂载,例如 <img /> 元素的 src 属性,可以使用 data-lazy-src 来替换,此时图片不会加载,因为 src 属性为空,类似:

<img height="400" data-lazy-src="./1663728157316fdd4ca.jpeg" alt="" />

然后监听页面滚动,计算元素当前位置进入视口后,主动设置图片的 src,类似:

window.onscroll = () => {
  const img = document.querySelector("img[data-lazy-src]");
  const rect = img.getBoundingClientRect();

  // 元素进入视口的条件,实际上有多种情况,这里只是判断元素顶部进入视口
  if (rect.top > 0 && rect.top < rect.height) {
    img.setAttribute("src", img.getAttribute("data-lazy-src"));
  }
};

此时图片不会一开始就加载,当滑动滚轮时,元素顶部进入视口后,图片才会加载。

来分析下这种方式可以进行的优化和其存在的缺陷。

优化:

  • 元素进入视口的条件很多,一个角,一条边,或者全部进入,又或者全部覆盖,这些条件都需要优化,类似碰撞监听。
  • 在计算时可以为视口大小进行偏移量填充,让元素距离视口一定偏移量就提前加载,提高加载体验。
  • 必须让懒加载一开始就执行,不然首屏的懒加载元素因为没有滚动,会无法加载资源。
  • 对回调进行节流,懒加载结束后关闭回调。

缺陷:

  • 元素不一定可以通过视口的滚动条进行展示,元素可能存在于滚动容器中,此时需要监听滚动的事件源就不止 window 了,并且此时懒加载触发会不正常,例如懒加载元素在父级滚动元素的隐藏区域,也在视口中,对于视口的监听就会触发懒加载执行,但实际上元素还是不可见的。

IntersectionObserver 实现懒加载 首选方案

IntersectionObserver 兼容性欠佳,使用时注意兼容性。

IntersectionObserver 是新规范中,用于观察元素和指定元素展示范围内的交叉状态。默认情况下,IntersectionObserver 观察元素和视口的交叉状态。

通过设置交叉区域观察交叉状态,可以知道当前元素是否可见,那么懒加载就得以实现。

通过 IntersectionObserver 创建一个一个交叉区域观察者,以下代码引用自 MDN:

const intersectionObserver = new IntersectionObserver((entries) => {
  // 如果 intersectionRatio 为 0,则目标在视野外,
  // 我们不需要做任何事情。
  if (entries[0].intersectionRatio <= 0) return;

  loadItems(10);
  console.log("Loaded new items");
});
// 开始监听
intersectionObserver.observe(document.querySelector(".scrollerFooter"));

实例化 IntersectionObserver 时可以传递观察的回调,回调的第一个参数是一个 IntersectionObserverEntry 类型的数组,记录了被观察的所有目标和其观察区域的相关交叉状态,其中 IntersectionObserverEntry.intersectionRatio 记录了交叉区域与被交叉元素位置的比例,所以只要该值大于 0,就说明产生了交叉。

回到之前的 dom 结构:

<img height="400" data-lazy-src="./1663728157316fdd4ca.jpeg" alt="" />

那么懒加载代码实现可以类似:

const intersection = new IntersectionObserver((entries, /* 交叉区域观察者的实例 */ instance) => {
  entries.map((item) => {
    if (item.intersectionRatio > 0) {
      const el = item.target;
      el.setAttribute("src", el.getAttribute("data-lazy-src"));
      // 加载资源后不再观察该元素(取消监听)
      instance.unobserve(el);
    }
  });
});

document.querySelectorAll("img[data-lazy-src]").forEach((item) => {
  // 使用交叉区域观察器观察相关懒加载元素(监听)
  intersection.observe(item);
});

上面代码实例化了一个 IntersectionObserver,并选取 img[data-lazy-src] 元素进行观察,当发现元素进入视口后,加载图片并取消对元素的观察。懒加载就这样轻松的实现了。

IntersectionObserver 还有其他配置、方法和属性,可以设置缩放观察区域,也可以设置观察区域由哪个元素确定。参见 API

IntersectionObserver 功能远不止实现懒加载,对于元素动画进入视口再次播放,高计算组件脱离可见区域停止计算等,都非常有用。

棘手的问题

无论使用哪种方式懒加载,都有一个问题就是图片在未加载时不知道其大小,未加载时总是 0 * 0,如果没有布局,可能会导致图片堆叠到一起,一次懒加载判断后会对大量堆叠在一起的图片进行加载,这就失去了懒加载的意义。解决方案只能通过布局或预先给图片设置大小,使其撑起页面。

参考和引用