图片懒加载的两种实现

7,559 阅读3分钟

引言

图片懒加载是前端性能优化中老生常谈的话题了,核心思想就是控制视口外的图片进入视口时候再进行加载,从而提升页面初始化渲染性能和用户体验。最近发现又有Intersection Observer这种新的API来更好的判断元素位置,因此将图片懒加载的技术进行一次回顾总结。

图片懒加载实现

图片通常采用<img>background两种形式来加载,那我们分别来演示两种情形的懒加载实现细节。

<img>标签形式的懒加载

首先,<img>标签通过src属性的url来加载图片,因此将需要懒加载的图片url添加到data-src属性,这样src为空就不会加载图片了。

<img data-src="https://img.dpm.org.cn/Uploads/Picture/2021/05/31/s60b446b015652.jpg">

然后,需要判断条件来触发图片加载,目前有两种方法,传统的事件触发和最新的Intersection Observer

通过js事件触发

需要注意的是有三种事件都可能导致图片的可视数量发生变化:scroll,resizeoritentionChange

  document.addEventListener("scroll", lazyload);
  window.addEventListener("resize", lazyload);
  window.addEventListener("orientationChange", lazyload);

当任意一个事件触发我们就需要判断需要懒加载图片是否在视口内,最常用的判断方法就是getBoundingClientRect

  function isInViewPort(element) {
    const viewWidth = window.innerWidth || document.documentElement.clientWidth;
    const viewHeight = window.innerHeight || document.documentElement.clientHeight;
    const {
      top,
      right,
      bottom,
      left,
    } = element.getBoundingClientRect	();
	// 这个是判断元素完整的在视口内
		/*     return (
      top >= 0 &&
      left >= 0 &&
      right <= viewWidth &&
      bottom <= viewHeight
    ); */
    // 这个是判断元素刚进入视口
	return (
      top >= 0 &&
      top <= viewHeight &&
      left >= 0
     );
  }

判断图片如果到了视口内,就将data-src的值赋给src,最后判断是否所有需要懒加载的图片已经完成,再移除事件。

  function lazyload() {
    if (lazyloadThrottleTimeout) {
      clearTimeout(lazyloadThrottleTimeout);
    }

    lazyloadThrottleTimeout = setTimeout(function() {
      lazyloadImages.forEach(function(img) {
        if (isInViewPort(img)) {
          img.src = img.dataset.src;
          img.classList.remove('lazy');
        }
      });
      if (lazyloadImages.length == 0) {
        document.removeEventListener("scroll", lazyload);
        window.removeEventListener("resize", lazyload);
        window.removeEventListener("orientationChange", lazyload);
      }
    }, 100);
  }

注意这里加了个定时器防止事件高频触发,其实就是一个简单的防抖函数debounce。完整代码见demo演示地址

Intersection Observer触发图片加载

Intersection Observer是一个比较新的api,目前不支持IE,用他来检测图片是否进入视口非常方便,不用再像之前绑定事件、计算距离等。下面看实现吧:

  if ("IntersectionObserver" in window) {
    lazyloadImages = document.querySelectorAll(".lazy");
    var imageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          var image = entry.target;
          image.src = image.dataset.src;
          image.classList.remove("lazy");
          imageObserver.unobserve(image);
        }
      });
    });

    lazyloadImages.forEach(function(image) {
      imageObserver.observe(image);
    });
  } 

将所有需要懒加载的图片注册observer,用isIntersecting属性监控,其他逻辑都一样的。完整代码见demo演示地址

background形式的懒加载

其实现在项目很少大面积的用img标签了,背景图片的使用比较多,处理其实也很简单,就是用css选择器来控制background-image是否加载。有lazy选择器时候background-image: none;,元素进入视口后移除选择器,相应图片的地址就可以加载出来了。

html

<div class="img img-1"></div>
<div class="img img-2"></div>
<div class="img img-3 lazy"></div>
<div class="img img-4 lazy"></div>
<div class="img img-5 lazy"></div>

css

.img {
  width: 400px;
  height: 300px;
  border: 1px solid #d9d9d9;
  margin: 20px auto;
  background-size: 100% 100%;
}
.img.lazy {
  background-image: none;
}
.img-1 {
  background-image: url(https://img.dpm.org.cn/Uploads/Picture/2021/05/31/s60b446b015652.jpg);
}
.img-2 {
  background-image: url(https://img.dpm.org.cn/Uploads/Picture/2021/05/31/s60b445921bdfe.jpg);
}

.img-3 {
  background-image: url(https://img.dpm.org.cn/Uploads/Picture/2021/05/31/s60b443ac90165.jpg);
}
.img-4 {
  background-image: url(https://img.dpm.org.cn/Uploads/Picture/2021/05/31/s60b4430138464.jpg);
}
.img-5 {
  background-image: url(https://img.dpm.org.cn/Uploads/Picture/2021/05/31/s60b441f616f18.jpg);
}

js:

document.addEventListener("DOMContentLoaded", function() {
  var lazyloadImages = document.querySelectorAll(".img.lazy");
  var lazyloadThrottleTimeout;

  function lazyload() {
    if (lazyloadThrottleTimeout) {
      clearTimeout(lazyloadThrottleTimeout);
    }

    lazyloadThrottleTimeout = setTimeout(function() {
      lazyloadImages.forEach(function(img) {
        if (isInViewPort(img)) {
          img.classList.remove('lazy');
        }
      });
      if (lazyloadImages.length == 0) {
        document.removeEventListener("scroll", lazyload);
        window.removeEventListener("resize", lazyload);
        window.removeEventListener("orientationChange", lazyload);
      }
    }, 20);
  }

  function isInViewPort(element) {
    const viewWidth = window.innerWidth || document.documentElement.clientWidth;
    const viewHeight = window.innerHeight || document.documentElement.clientHeight;
    const {
      top,
      right,
      bottom,
      left,
    } = element.getBoundingClientRect	();
		// 这个是判断元素完整的在视口内
		/*     return (
      top >= 0 &&
      left >= 0 &&
      right <= viewWidth &&
      bottom <= viewHeight
    ); */
    // 这个是判断元素刚进入视口
		return (
      top >= 0 &&
      top <= viewHeight &&
      left >= 0
     );
  }

  document.addEventListener("scroll", lazyload);
  window.addEventListener("resize", lazyload);
  window.addEventListener("orientationChange", lazyload);
});

完整代码见demo演示地址

总结

图片懒加载的基本原理很简单,后续还有很多优化的手段,包括占位图片的使用来提升用户体验,还有图片的压缩等,以后实践中慢慢补充。