图片懒加载完全指导(上)

2,532 阅读9分钟

现在无论是对于网站还是App应用,图片都成了至关重要的组成部分,无论是营销横幅、商品图片,还是logo,一个网站如果连一幅图都没有,那对于浏览体验来说是不可想象的。而与此同时,图片的大小也顺理成章地成为每次请求资源中的大头。根据网站Http Archive当前时间点的统计数据,桌面和移动设备的页面请求资源的平均总量分别是:1828.2KB1682.4KB,而图片在其中的所占据的大小分别为:899.6KB862.9KB,可以粗略的计算出图片的占比分别是:49%51%。这说明当我们在考虑网站的性能优化问题时,已经不能无视图片的存在了。这篇文章我们将讨论图片懒加载技术,即在保留页面中所有图片资源的同时,如何来提升页面加载速度和减小页面大小。

在深入探讨之前,先来看一个图片懒加载的实例,观察到在滚动页面时,显示在屏幕视窗中的图片才会发起请求,等拿到真实图片后便替换原本的占位空间。

1. 图片的懒加载

图片懒加载指的是Web及应用开发中相关的一组技术,指通过将图片的请求加载时间点,推迟到不得不展示时再去请求加载,而非一开始就去请求页面所包括的全部图片资源。这将有效地提升性能、改善设备资源的利用率以及减少相关的开销。

其实可以对页面上几乎任何资源采取懒加载技术,比如一个单页面应用中,如果一个js文件不需要马上使用,那么最好就不要在初始化的时候加载它。同样一张图片如果不会在首屏中展示,那么最好的做法就是能要展示的时候,再加载它。

本文将关注图片的懒加载以及如何在网站中更优雅的实现。

1.1 懒加载的重要性

理解了图片懒加载的原理,可以明显看出这项功能所带来的两个好处:

1. 性能优化

对于网站管理员来说最重要的事情,莫过于良好的性能与加载时间。采用了懒加载后,便会减少网站页面初始化时需要加载的图片数量。较少的资源请求意味着,对于用户网络带宽较少的占用。这也将确保对剩下资源更快的下载与处理速度,好处显而易见。

2.2. 降低成本

另一个好处是传输成本方面,传输图片或其它任何类型的资源时,通常都是以所传输的字节数来计费的。如前所述,懒加载对于不展示的图片,就不会加载,因此这就降低了页面传输的总字节数。这对于只浏览页面顶部或打开后就关闭的用户来说,特别节约成本。这一点也将在本文后续的探讨中更加明显。

1.2 哪些图片可以做懒加载?

图片懒加载的基本想法很简单,对于不是立马需要的资源都延迟加载。对于图片来说,凡是不在首屏视窗中的图片都可以做懒加载,而当用户滚动页面,图片的占位空间出现在视窗后,再触发图片资源的请求然后加载。

另外我们可以使用一些工具来识别,哪些图片应该进行懒加载以减少页面初始化时的字节传输。这里推荐两个工具:

  • Lighthouse,Google的前端性能分析工具,生成的报告细致入微而且很有指导意义,后面有机会我会单独写文章做细致分析。
  • ImageKit,在线性能分析工具,免费版可主要针对网站所使用的图片进行分析,并会给出相应的优化建议。

可以看出懒加载对性能提升和优化用户体验都很重要,接下来我们将详细的介绍各种实现懒加载的方式。

2. 懒加载的实现

常见的网页加载图片有两种方式:使用<img>标签;使用CSS的background属性。

2.1 <img>标签方式

懒加载可以被拆解成两步:

  1. 防止图片一上来就被加载。浏览器会检查<img />标签的src属性来触发图片的加载,不论是第一张图片或是还未出现在屏幕视窗中的第1000张图片都无所谓,只要浏览器在解析HTML后,发现<img>标签的src属性上有图片的URL,就会发起请求加载图片。所以实现懒加载的第一步,就是将图片的URL存储在一个非src属性上,可以是data-src,而将src属性值暂设为空。 <img data-src="https://xxx/image.jpg" src="" />
  2. 触发图片加载。当滚动页面等操作,使得图片出现在屏幕视窗中时,触发图片的加载。这一步的关键是检测图片出现在视窗中的时刻,接下来介绍两种实现方式。

JavaScript事件方式

这里将使用到三个检测浏览器视窗改变的事件:scrollresizeorientationChange

  • scroll:用户滚动页面时触发
  • resize:当浏览器窗口尺寸改变时触发
  • orientationChange:一般针对移动端,当设备在横屏与竖屏模式下切换时触发

这些改变的时刻,都将有可能使之前一些未出现在浏览器视窗中的图片现在出现在了视窗中。当这些事件触发时,通过检查每个未曾加载图片位置信息,来判断其当前是否出现在视窗中,判断方法如下图所示: evernotecid://97E3A084-2194-4244-AC3A-BA8AC29EED05/appyinxiangcom/7428242/ENResource/p341

当图片出现在视窗上后,便将其data-src的属性赋值给src属性,来触发图片的加载,同时移除标识为懒加载的类名lazy,防止后续事件再次触发后重复加载。最后当所有待懒加载的图片都加载完毕后,移除事件监听。另外,容易看出scroll事件会被多次重复触发,出于性能考虑,可以设置一小段延迟,来限制回调函数的即时执行。

document.addEventListener("DOMContentLoaded", function() {
  var lazyloadImages = document.querySelectorAll("img.lazy");    
  var lazyloadThrottleTimeout;
  
  function lazyload () {
    if(lazyloadThrottleTimeout) {
      clearTimeout(lazyloadThrottleTimeout);
    }    
    
    lazyloadThrottleTimeout = setTimeout(function() {
        var scrollTop = window.pageYOffset;
        // 遍历检查所有懒加载图片
        lazyloadImages.forEach(function(img) {
            if(img.offsetTop < (window.innerHeight + scrollTop)) {
              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);
        }
    }, 20);
  }
  // 添加事件监听
  document.addEventListener("scroll", lazyload);
  window.addEventListener("resize", lazyload);
  window.addEventListener("orientationChange", lazyload);
});

对于首页就在视窗中展示的图片,无需通过事件触发才进行加载处理,出于用户体验考虑,直接加载就好。

Intersection Observer API方式

Intersection Observer API是一个相对较新的API,在此之前,为了判断一个元素是否出现在视窗,并做响应的处理操作,我们只能像上一节介绍的那样:绑定事件后在处理函数中,通过相关位置属性值的计算,来确定元素是否出现在视窗。姑且先不考虑这种方式的性能表现,单就计算的繁琐就算不上优雅的实现。

下面使用Intersection Observer API来实现懒加载:通过将观察者对象绑定到所有待加载的图片上后,当图片元素出现在视窗上触发监听,判断isIntersecting属性来将<img>标签data-src上的URL赋值给src属性以加载图片。具体代码如下:

 document.addEventListener("DOMContentLoaded", function() {
  var lazyloadImages;    
  // 浏览性兼容性判断
  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);
    });
  } else {  
    // 对于不兼容intersection observer API的浏览器使用事件绑定方式
    ...
  }
})

如果我们来比较这两种实现懒加载的方式,显然使用Intersection Observer API的方式对于图片加载的触发时刻会比事件监听的方式更快。同时如同所有新方法一样,优良的表现背后都隐藏着兼容性的劣势,对于不支持此API的浏览器我们在使用的时候,还需注意兼容处理。 evernotecid://97E3A084-2194-4244-AC3A-BA8AC29EED05/appyinxiangcom/7428242/ENResource/p342

2.2 使用CSS的Background方式

CSS的background在页面中显示图片的方式,和上面<img>的方式相比可能不是那么直观。使用这种加载图片的方式,浏览器需要先构建DOM树和CSSOM树后,以确定当前文档的DOM节点上CSS的样式。如果指定了background-image的CSS规则没有应用在文档的元素上,浏览器就不会加载相应的图片,反之只有background-image的CSS规则应用于文档某元素上时,浏览器才会请求加载对应图片。这一性质也就成了该方式实现懒加载的原理。如下HTML中的标签将不仅限于<img>

<div id="bg-image" class="lazy"></div>

CSS属性设置如下

#bg-image.lazy {
   background-image: none;
   background-color: #F1F1FA;
}
#bg-image {
  background-image: url("https://xxx/image10.jpeg");
  max-width: 600px;
  height: 400px;
}

初始化情况下,background-image属性为none不加载图片。判断图片加载时刻所用的方法,沿用上一节intersection observer API或事件监听都可以,当需要加载时,移除元素类名lazy后,background-image非空的CSS规则就会覆盖之前的规则,从而发起对图片资源的请求并加载之。