深入理解图片懒加载:原理、实现与性能优化

156 阅读7分钟

深入理解图片懒加载:原理、实现与性能优化

google-76517_1280.webp

图片作为网页中重要的组成部分,其加载方式对页面性能有着显著影响。懒加载技术是提升网页加载速度和用户体验的关键手段。

一、什么是图片懒加载

图片懒加载,也被称为延迟加载,是一种在长网页中延迟加载图片的策略。它不是在加载时将所有图片一次性全部加载的模式,而是根据用户的浏览行为,仅在图片即将进入可视区域时才进行加载,从而有效节省资源,提升网页性能。

二、为什么需要图片懒加载

要理解为什么需要图片懒加载,我们需要理解图片加载的原理,以及网络传输的相关知识

  1. 图片加载的底层原理

    当浏览器解析 HTML 代码时,预加载扫描器(preload scanner)会并行处理<img>标签,创建 HTMLImageElement对象并加入DOM树 。此时若<img>中存在有效的 src 属性,浏览器会立即通过网络进程发起异步资源请求。(无需等待DOM/CSSOM构建完成)

  2. 图片请求

    在每一次图片资源请求中,浏览器首先会创建一个 TCP 连接,通过三次握手建立通信,之后服务器返回图片资源最后浏览器解析图片资源并将其显示在页面上

  3. 网络带宽

    网络带宽就如同一条有限宽度的公路,单位时间内能够承载的数据传输量是有限的。如果在网页加载时一次性请求加载过多的图片,就好比公路突然涌入大量车辆,导致交通阻塞,最终可能使得页面崩溃(加载不出来),严重影响用户体验。

    同时,页面的打开速度是衡量用户体验的重要指标,而大量图片的同时加载会显著拖慢页面加载速度,降低用户的满意度和留存率。因此,采用图片懒加载技术显得尤为必要。

三、如何实现图片懒加载

实现懒加载需要解决的问题

  1. <img>标签只要存在src属性且有地址就会发起请求,所以不能直接将图片的真实路径赋给src属性。要解决这个问题,可以将真实图片的 URL 存储在自定义属性中,比如data-original,而src属性则设置为一个占位符,例如一个透明的 GIF 图像。

    • data-original,这是一个自定义的数据属性,专门用于存储图片的路径。只有当图片滚动到视口中时,才将data-original的值赋给src属性,触发图片的真实加载。

          <img
            class="image-item"
            lazyload="true"
            src="预加载图片地址"
            data-original="真实图片地址"
          />
      
    • 占位符的作用在于先占据图片的位置,并且由于其体积较小,加载快,加载后会被缓存,后续其他<img>标签加载时可以直接从缓存中获取,从而提高加载效率。占位图片如下:

    20250627.gif

  2. 图片懒加载的核心思路是只加载可视区域内的图片。要实现图片懒加载需要判断<img>标签是否出现在可视区域,还需要考虑浏览器窗口的大小以及页面的滚动区域。

    • 判断一个元素是否在可视区域中有多种方式,这里以getBoundingClientRect()方法为例

      使用DOM对象调用getBoundingClientRect后返回值是一个 DOMRect对象,该对象拥有left, top, right, bottom, x, y, width, 和 height属性。其属性如下

      // 典型DOMRect结构
      {
        bottom: 328,// 下边界到视口顶部的距离
        top: 128,   // 上边界到视口顶部的距离
        x: 52,      // 矩形左上角X坐标(相对于视口)
        y: 128,     // 矩形左上角Y坐标
        width: 300, // 内容宽度(包含padding)
        height: 200,// 内容高度
        right: 352, // 右边界X坐标(x + width)
        left: 52    // 左边界X坐标(等同x)
      }
      

      这里我们只需要使用到其中的三个属性,使用height获取视窗的高度;使用bottom获取元素底部到视口顶部的距离;使用top获取元素顶部到视口顶部的距离。当top小于视窗高度,且bottom大于0时则该元素在视窗内。

      以一个id为react的元素为例:

        const react = document.getElementById("react");
        // 1. 获取视窗高度
        const viewHight = document.getBoundingClientRect().height;
      
        // 2. 获取react 顶部和底部到视口顶部的距离
        const reactTop = react.getBoundingClientRect().top;
        const reactBottom = react.getBoundingClientRect().bottom;
      
        // 3. 判断
        if (reactTop < viewHight && reactBottom > 0) {
          // 在视口中
          
        }
      
      
  3. 修改出现在视窗内的img标签的src属性,将其值改为data-origin中的真实图片地址。

    在实际实现图片懒加载时,通常需要遍历页面中的所有<img>标签,结合上述判断元素是否在可视区的方法,对处于可视区域的图片进行加载操作。

    代码实现如下:

    // document.documentElemt 表示获取文档根元素 html
    // 获取视口高度
    const viewHeight = document.documentElement.clientHeight;
    // 获取所有的懒加载img
    const images = document.querySelectorAll('img[data-original][lazyload="true"]');
    
    const lazyLoad = function () {
      Array.prototype.forEach.call(images, function (item) {
        // 若没有值,直接返回
        if (item.dataset.original === "") return;
        // 获取图片距离视窗顶部的距离
        const rectTop = item.getBoundingClientRect().top;
    
        // 获取图片距离视窗底部的距离
        const reactBottom = item.getBoundingClientRect().bottom;
    
        if (rectTop <= viewHeight && reactBottom >= 0) {
          (function () {
            //这种设计的核心目的是 避免页面闪烁和破碎图片占位符
    
            // 预加载 其作用是创建一个内存中的图片对象,
            // 然后 img.src = item.dataset.original 触发图片资源的 后台加载
            // 此时图片不会显示在页面中,仅在浏览器后台缓存图片数据
            // 当再次请求该图片地址的时候会从缓存区中拿图片
            // 这样做的好处是
            // 1. 避免页面闪烁和破碎图片占位符
            // 2. 避免图片加载顺序问题
            let img = new Image();
            img.src = item.dataset.original;
    
            // 当临时图片对象 img 触发 onload 事件(表示图片已完全加载完成)
            img.onload = function () {
              item.src = item.dataset.original;
              
              // 垃圾回收
              item.removeAttribute("data-original");
              // 移除自定义属性
              item.removeAttribute("lazyload");
            };
          })();
        }
      });
    };
    window.addEventListener("scroll", lazyLoad);
    document.addEventListener("DOMContentLoaded", lazyLoad);
    
    

    上述代码的逻辑如下:

    • 获取视口高度和所有带 data-originallazyload="true"属性的懒加载图片
    • 当屏幕滚动时遍历所有图片,通过getBoundingClientRect()检测图片是否进入可视区域
    • 对符合条件的图片创建临时Image对象进行预加载,当预加载完成后将真实地址赋给图片元素,并移除懒加载相关属性。
  4. 效果图

20250628-2308.gif

四、性能优化

  1. 存在的性能问题

    • 首先是首屏加载,如果首屏图片过多且没有合理优化,仍然会影响页面的初始加载速度。
    • 频繁地触发onScroll事件会带来性能损耗,因为每次触发onScroll都需要遍历所有的<img>标签,判断其是否在可视区域内,这会占用大量的计算资源。
    • 每次调用img.getBoundingClientRect()方法都会触发回流,频繁的回流操作会严重影响页面性能,这在实际开发中是不可接受的。
  2. 优化性能

    InterSectionObserver是浏览器提供的异步观察 API,属于 HTML5 时代之后的现代 Web API 标准,它专门用于高效监听元素与视口的交叉状态

    使用InterSectionObserver后,不再需要依赖onScroll事件以及getBoundingClientRect,大大减少了事件触发的频率和不必要的计算,同时减少回流的发生,从而可以显著提升图片懒加载的性能,为用户带来更加流畅的网页浏览体验。

     function addObserver() {
            const eles = document.querySelectorAll("img[data-original][lazyload]");
    		
         	// 为每个图片创建独立的观察者实例
            eles.forEach((item) => {
              //设计模式中的观察者   传递一个回调函数-change是一个数组,这个数组里面的内容
              // change是观察的所有
              const observer = new IntersectionObserver(function (change) {
                change.forEach(function (element) {
                  // 图片进入视口
                  if (
                    element.intersectionRatio > 0 &&
                    element.intersectionRatio <= 1
                  ) {
                    const img = new Image();
                    img.src = element.target.dataset.original;
                    img.onload = function () {
                      element.target.src = element.target.dataset.original;
                    };
                  }
                });
              });
    
              observer.observe(item);
            });
          }
          
          addObserver();
    

    上述代码的逻辑如下:

    • 使用 document.querySelectorAll 选择所有带有data-originallazyload属性的 标签
    • 为每个图片创建独立的观察者实例
    • 当图片进入视口时(通过intersectionRatio判断可见比例)
    • 创建临时Image对象进行预加载
    • 图片加载完成后将临时地址赋值给目标元素的src属性