使用IntersectionObserver优化图片加载

avatar
@智云健康

作者:maxin,未经授权禁止转载。

前言

工作中经常通过判断元素是否进入视口,一般有三种方式进行判断

1. el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
2. el.getBoundingClientRect().top <= viewPortHeight
3. intersectionRatio > 0 && intersectionRatio <= 1

根据元素与视口是否相交,可以进行吸顶、吸底、曝光上报、列表加载更多、图片懒加载等操作。

问题

前面两种需要通过监听scroll事件,为了防止频繁触发,需要做防抖处理。

用户体验

  • 当元素进入视口时,总是需要延迟一定时间才能执行判断逻辑。

性能

  • 在主线程上运行,因此频繁触发、调用会造成性能问题
  • 无论是否触发相交,滚动结束后都会进行判断
  • 获取srollTop的值和getBoundingClientRect方法都会导致回流
  • 滚动事件会绑定多个事件处理函数,阻塞UI渲染

IntersectionObserver

当目标元素和根元素相交或失交,并且当前 tick 期间没有任务运行时,回调函数就会被执行

构造函数

let observer = new IntersectionObserver(callback, options);

callback

每个entry描述一个观察到的目标元素的交集变化

let callback = (entries, observer) => {
  entries.forEach(entry => {
    // entry.boundingClientRect
    // 目标元素的区域信息,getBoundingClientRect()的返回值
    
    // entry.intersectionRatio
    // 目标元素的可见比例
    
    // entry.intersectionRect
    // 目标元素与根元素交叉的区域信息
    
    // entry.isIntersecting
    // 目标元素是否进入根元素区域
    
    // entry.rootBounds
    // 根元素的矩形区域信息
    
    // entry.target
    // 被观察dom节点
    
    // entry.time
    // 相交发生时距离页面打开时的毫秒数
  });
};

options

  • root 根元素,不指定默认为视窗
  • rootMargin 根元素的外边距
  • threshold 目标元素与根元素相交比例达到该值触发回调

实例方法

  • observe 开始监听一个目标元素(target),target必须是root的后代
  • unobserve 停止监听一个目标元素
  • takeRecords 返回所有监听的目标元素集合
  • disconnect 停止所有监听

参考

MDN Web Docs - Intersection Observer API

vue directive优化懒加载

步骤

防止直接加载图像,先把图片地址存放在data-src上

<img
  class="image-item"
  :data-src="imageUrl"
  v-lazyload
/>

LazyLoadDirective.js

export default {
  // hookFunction 当绑定元素插入父节点时调用
  insert(el) {
    function loadImage() {
      el.addEventListener('load', () => {
        // 加载完成后延迟添加class可以实现淡入动画
        setTimeout(() => {
          el.classList.add('loaded')
        }, 100);
      });

      // 加载 data-url 的图片地址
      el.src = el.dataset.url;
    }

    function handleIntersect(entries, observer) {
      entries.forEach(entry => {
        if (!entry.isIntersecting) {
          return;
        } else {
          // 绑定元素进入视口后触发加载图片
          loadImage();
          // 并停止观察可见性变化, 防止再次加载图像。
          observer.unobserve(el);
        }
      });
    }

    function createObserver() {
      const options = {
        root: null,
        threshold: '0'
      };

      const observer = new IntersectionObserver(handleIntersect, options);

      // 订阅观察当前绑定图片元素
      observer.observe(el);
    }
    
    createObserver();
  }
}

思考

如果使用scroll事件来判断图片可见或不可见,每次需要重新计算的几十个图像,显然使用IntersectionObserver更加优雅。

注册指令

// main.js 全局注册
import Vue from 'vue';
import LazyLoadDirective from '@/directives/LazyLoadDirective';

Vue.directive('lazyload', LazyLoadDirective);

小结

使用IntersectionObserver实现延迟加载非常简单,拥有很好的性能和用户体验,目前系统兼容性覆盖的比较广。当然,如果列表元素节点特别多时,也需要进行长列表优化。

结尾

感谢你的阅读,日前智云健康大前端团队正在参加掘金人气团队评选活动。如果你觉得还不错的话,那就来 给我们投几票 吧!

今日总共可以投21票,网页7票,App7票,分享7票。感谢支持,2021我们还会创作更多的技术好文~~~

你的支持是是我们最大的动力~