【硬核】如何写一个自己的图片懒加载函数

当今的 web 页面

回顾一下浏览器从0开始加载网页的过程

现在访问一个 web 页面一般会经过这样的简易步骤:

  1. 用户在浏览器中输入 web 页面对应的域名
  2. 域名经过 DNS 服务器解析成对应的 IP 地址(如果是合法的话)
  3. 客户端与服务端通过三次握手建立 TCP 连接
  4. 浏览器(客户端)向服务器发起 HTTP 请求一个静态的 HTML 页面(e.g index.html,detail.html, home.html ....)
  5. 加载 css (不阻塞) 和 js (一般情况下阻塞) 文件
  6. 遇到一些需要加载资源的标签,比如imgvideoaudio这些标签时,它们会再次向服务器发送 HTTP 请求获取这些标签所需要的资源文件。
  7. ...
  8. 在最后时刻,通过四次挥手断开 TCP 连接

单服务器处理用户访问可能存在的问题

在浏览器从0开始加载网页的过程的第6步,如果当前页面对应的业务存在许多的资源,比如:

  • 电商网站(淘宝,京东)
  • 视频网站(B站、油管、抖音)
  • 新闻网站(今日头条、网易新闻)
  • ...

如果在单个服务器上存在大量的访问并且同时加载页面,必定会增加单个服务器的负担导致服务响应较慢甚至超时。

简单地实现服务器的分流

于是针对上述情况,现在很多网页都是这样处理的:

  1. 把所有的静态 HTML 页面放在自己的服务器上面
  2. 把脚本文件(css、js)放在带宽较高的托管资源服务器上面(e.g 七牛云、阿里云 ...)
  3. 把资源文件(图片、音频、视频)或单个、或切片的放在带宽较高的托管资源服务器上面(e.g 七牛云、阿里云 ...)

百度举例:

  • 请求 HTML
  • 请求脚本文件


  • 请求资源文件

那既然是这样,为什么还要图片优化呢?

服务器分流只是在服务器端的流量分配上面优化了服务器的带宽,但是它并不能解决用户在客户端(e.g 电脑,手机)上面可能还会存在的问题:

  • 网络问题
    1. 网络巨卡,资源加载不出来,导致白屏
    2. 网络很卡,资源能加载但是加载缓慢,存在卡顿
    3. 网络小卡,资源能加载一部分,但是另外一部分白屏或者卡屏
  • 用户体验问题
    1. 图片加载太慢,卡顿或者白屏
    2. 图片全部正常加载并直接显示,用户感知力不强

解决客户端可能存在问题的方式:

  1. 图片的懒加载
  2. 图片的预加载
  3. 显示前的骨架屏
  4. 雪碧图

本文着重讲述如何使用图片懒加载技术完成多图片网页的加载显示。

首先,什么是图片懒加载?

懒加载,顾名思义:相较于普通加载,它可以:

  1. 先统一加载一张 loading 图片(只加载一张图片,而不是全部一次性加载)
  2. 在图片滚动到可视区域范围内,把真正需要的图片地址替换掉之前加载的 loading 图片地址, 并且显示出来。

总体来说,懒加载以总体多加载一张 loading 图片的代价,让其他的图片能够按照特定的条件和时机进行加载,给予了用户一定的缓冲时间。对于一些多图的网站来讲,懒加载是一种不错的选择方案。

懒加载的整体思路:

懒加载的整体步骤如下:

  1. 列表渲染所有的图片,把图片对应的 <img> 标签的 src 属性设置为 loading 图片的地址
html 复制代码
<div class="img-list J_ImgList">
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/4.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
</div>
  1. 在图片对应的 <img> 标签设置一个自定义的属性 data-src, 它存放的值为真正需要加载的图片地址的值。
html 复制代码
<div class="img-list J_ImgList">
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/1.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/2.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/3.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/4.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/5.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/6.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/7.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/8.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/9.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/10.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/11.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/12.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/13.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/14.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/15.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/16.jpg" alt="" />
  </div>
</div>
  1. 写一下样式
css 复制代码
html,
body {
  margin: 0;
  font-size: 15px;
  color: #3d3d3d;
}

img {
  display: block;
  width: 100%;
  height: 100%;
}

.img-list {
  width: 100%;
}

.img-list::after {
  content: "";
  display: table;
  clear: both;
}

.img-list .img-list-item {
  width: 25%;
  float: left;
  height: 500px;
  padding: 5px;
  box-sizing: border-box;
}
  1. 写一个 IIFE 演示模块
javascript 复制代码
;(function () {
  var init = function () {
    bindEvent();
  }

  function bindEvent() {}

  init();
})();
  1. 获取这些图片元素,绑定事件处理函数 handleScroll
javascript 复制代码
;(function (doc) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');
  
  var init = function () {
    bindEvent();
  }

  function bindEvent() {
    window.addEventListener('scroll', handleScroll, false);
  }

  function handleScroll() {
    // do something
  }

  init();
})(document);
  1. 封装一个函数 lazyLoadImg() 判断这些图片是否处于可视范围之内,如果处于可视范围之内并且存在 data-src属性,使用 data-src替换掉原来的 src的值,删除 data-src
javascript 复制代码
/**
 * @function lazyLoadImg
 * @description 懒加载图片元素
 * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
 * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
 * @param {number} clientHeight 容器的整体高度
 * @param {number} scrollTop 当前容器已滚动的高度
 * @return {void} 没有返回值
 */
function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
  for (var i = 0; i < oImgItems.length; i++) {
    var oImgItem = oImgItems[i];
    var oImg = oImgItem.getElementsByTagName('img')[0];
    var offsetTop = oImgItem.offsetTop;
    
    if (clientHeight + scrollTop >= offsetTop) {
      if (!!oImg.getAttribute('data-src')) {
        oImg.setAttribute('src', oImg.getAttribute('data-src'));
        oImg.removeAttribute('data-src');
      }
    }
  }
}
  1. 设置lazyLoadImg()在 init 时执行; 在 handleScroll 的时候,也执行一下 lazyLoadImg()
javascript 复制代码
;(function (doc) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');

  var init = function () {
    lazyLoadImg(
      oImgItems,
      document.documentElement.clientHeight,
      document.documentElement.scrollTop || document.body.scrollTop
    );
    bindEvent();
  }

  function bindEvent() {
    window.addEventListener('scroll', utils.throttle(handleScroll), false);
  }

  function handleScroll(e) {
    var clientHeight = document.documentElement.clientHeight;
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    lazyLoadImg(oImgItems, clientHeight, scrollTop);
  }

  /**
   * @function lazyLoadImg
   * @description 懒加载图片元素
   * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
   * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
   * @param {number} clientHeight 容器的整体高度
   * @param {number} scrollTop 当前容器已滚动的高度
   * @return {void} 没有返回值
   */
  function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
    for (var i = 0; i < oImgItems.length; i++) {
      var oImgItem = oImgItems[i];
      var oImg = oImgItem.getElementsByTagName('img')[0];
      var offsetTop = oImgItem.offsetTop;
      
      if (clientHeight + scrollTop >= offsetTop) {
        if (!!oImg.getAttribute('data-src')) {
          oImg.setAttribute('src', oImg.getAttribute('data-src'));
          oImg.removeAttribute('data-src');
        }
      }
    }
  }

  init();
})(document);
  1. 在 init 时执行时, 需要将页面滚动到顶部。
javascript 复制代码
;(function (doc) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');

  var init = function () {
    window.scroll(0, 0);
    lazyLoadImg(
      oImgItems,
      document.documentElement.clientHeight,
      document.documentElement.scrollTop || document.body.scrollTop
    );
    bindEvent();
  }

  function bindEvent() {
    window.addEventListener('scroll', utils.throttle(handleScroll), false);
  }

  function handleScroll(e) {
    var clientHeight = document.documentElement.clientHeight;
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    lazyLoadImg(oImgItems, clientHeight, scrollTop);
  }

  /**
   * @function lazyLoadImg
   * @description 懒加载图片元素
   * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
   * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
   * @param {number} clientHeight 容器的整体高度
   * @param {number} scrollTop 当前容器已滚动的高度
   * @return {void} 没有返回值
   */
  function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
    for (var i = 0; i < oImgItems.length; i++) {
      var oImgItem = oImgItems[i];
      var oImg = oImgItem.getElementsByTagName('img')[0];
      var offsetTop = oImgItem.offsetTop;
      
      if (clientHeight + scrollTop >= offsetTop) {
        if (!!oImg.getAttribute('data-src')) {
          oImg.setAttribute('src', oImg.getAttribute('data-src'));
          oImg.removeAttribute('data-src');
        }
      }
    }
  }

  init();
})(document);

代码优化:

优化点一:

滚动触发太频繁,使用节流函数控制单位时间内触发滚动的频率:

javascript 复制代码
;(function (doc) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');

  var init = function () {
    bindEvent();
  }

  function bindEvent() {
    window.scroll(0, 0);
    lazyLoadImg(
      oImgItems,
      document.documentElement.clientHeight,
      document.documentElement.scrollTop || document.body.scrollTop
    );
    window.addEventListener('scroll', throttle(handleScroll, 300), false);
  }

  function handleScroll(e) {
    var clientHeight = document.documentElement.clientHeight;
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    lazyLoadImg(oImgItems, clientHeight, scrollTop);
  }

  /**
   * @function lazyLoadImg
   * @description 懒加载图片元素
   * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
   * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
   * @param {number} clientHeight 容器的整体高度
   * @param {number} scrollTop 当前容器已滚动的高度
   * @return {void} 没有返回值
   */
  function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
    for (var i = 0; i < oImgItems.length; i++) {
      var oImgItem = oImgItems[i];
      var oImg = oImgItem.getElementsByTagName('img')[0];
      var offsetTop = oImgItem.offsetTop;
      
      if (clientHeight + scrollTop >= offsetTop) {
        if (!!oImg.getAttribute('data-src')) {
          oImg.setAttribute('src', oImg.getAttribute('data-src'));
          oImg.removeAttribute('data-src');
        }
      }
    }
  }

  init();
})(document);
javascript 复制代码
/**
 * @function throttle
 * @description 节流函数
 * @param {Function} fn 需要被节流执行的函数
 * @param {number | undefined} delay 需要被节流的时长 (默认是 300 ms)
 * @return {Function} 返回一个被节流的执行函数
 */
function throttle(fn, delay) {
  var _delay = typeof delay === 'number' && delay > 0 ? delay : 300;
  var timer = null;
  var startTime = Date.now();

  return function () {
    var args = arguments;
    var ctx = this;
    var endTime = Date.now();

    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    if (endTime - startTime >= _delay) {
      startTime = endTime;
    } else {
      timer = setTimeout(function () {
        fn.apply(ctx, args);
        startTime = Date.now();
        clearTimeout(timer);
        timer = null;
      }, endTime - startTime);
    }
  }
}

优化点二:

如果图片滚动到最底部,图片列表的懒加载就会失效。

这里需要在 init 的时候就把窗口滚动到最顶部,并且其他的程序延迟执行。

javascript 复制代码
;(function (doc) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');

  var init = function () {
    var t = setTimeout(function() {
      window.scrollTo(0, 0);
      lazyLoadImg(
        oImgItems,
        document.documentElement.clientHeight,
        document.documentElement.scrollTop || document.body.scrollTop
      );
      bindEvent();

      clearTimeout(t);
      t = null;
    }, 300);
  }

  function bindEvent() {
    window.addEventListener('scroll', utils.throttle(handleScroll), false);
  }

  function handleScroll(e) {
    console.log('handleScroll', e);
    var clientHeight = document.documentElement.clientHeight;
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    lazyLoadImg(oImgItems, clientHeight, scrollTop);
  }

  /**
   * @function lazyLoadImg
   * @description 懒加载图片元素
   * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
   * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
   * @param {number} clientHeight 容器的整体高度
   * @param {number} scrollTop 当前容器已滚动的高度
   * @return {void} 没有返回值
   */
  function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
    for (var i = 0; i < oImgItems.length; i++) {
      var oImgItem = oImgItems[i];
      var oImg = oImgItem.getElementsByTagName('img')[0];
      var offsetTop = oImgItem.offsetTop;
      
      if (clientHeight + scrollTop >= offsetTop) {
        if (!!oImg.getAttribute('data-src')) {
          oImg.setAttribute('src', oImg.getAttribute('data-src'));
          oImg.removeAttribute('data-src');
        }
      }
    }
  }

  init();
})(document);

优化点三:

lazyLoadImg需要作为一个纯函数,把它提取到工具外面,然后将工具导入到模块中使用

javascript 复制代码
var utils = (function () {
  /**
   * @function lazyLoadImg
   * @description 懒加载图片元素
   * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
   * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
   * @param {number} clientHeight 容器的整体高度
   * @param {number} scrollTop 当前容器已滚动的高度
   * @return {void} 没有返回值
   */
  function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
    for (var i = 0; i < oImgItems.length; i++) {
      var oImgItem = oImgItems[i];
      var oImg = oImgItem.getElementsByTagName('img')[0];
      var offsetTop = oImgItem.offsetTop;
      
      if (clientHeight + scrollTop >= offsetTop) {
        if (!!oImg.getAttribute('data-src')) {
          oImg.setAttribute('src', oImg.getAttribute('data-src'));
          oImg.removeAttribute('data-src');
        }
      }
    }
  }

  /**
   * @function throttle
   * @description 节流函数
   * @param {Function} fn 需要被节流执行的函数
   * @param {number | undefined} delay 需要被节流的时长 (默认是 300 ms)
   * @return {Function} 返回一个被节流的执行函数
   */
  function throttle(fn, delay) {
    var _delay = typeof delay === 'number' && delay > 0 ? delay : 300;
    var timer = null;
    var startTime = Date.now();

    return function () {
      var args = arguments;
      var ctx = this;
      var endTime = Date.now();

      if (timer) {
        clearTimeout(timer);
        timer = null;
      }

      if (endTime - startTime >= _delay) {
        startTime = endTime;
      } else {
        timer = setTimeout(function () {
          fn.apply(ctx, args);
          startTime = Date.now();
          clearTimeout(timer);
          timer = null;
        }, endTime - startTime);
      }
    }
  }

  return {
    lazyLoadImg: lazyLoadImg,
    throttle: throttle,
  };
})();
javascript 复制代码
;(function (doc, utils) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');

  var init = function () {
    var t = setTimeout(function() {
      window.scrollTo(0, 0);
      utils.lazyLoadImg(
        oImgItems,
        document.documentElement.clientHeight,
        document.documentElement.scrollTop || document.body.scrollTop
      );
      bindEvent();

      clearTimeout(t);
      t = null;
    }, 300);
  }

  function bindEvent() {
    window.addEventListener('scroll', utils.throttle(handleScroll), false);
  }

  function handleScroll(e) {
    console.log('handleScroll', e);
    var clientHeight = document.documentElement.clientHeight;
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    utils.lazyLoadImg(oImgItems, clientHeight, scrollTop);
  }

  init();
})(document, utils);

整体代码实现:

img-lazyload-playground.zip

相关推荐
Cao_Ron4 分钟前
用 SVG image symbol 画 ECharts graph 圆形节点
前端
hxy06019 分钟前
React-Call让你的React组件像异步函数一样调用
前端·react.js
Hilaku10 分钟前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
京东云开发者12 分钟前
GPT-5.6、ChatGPT Work 与 Codex 更新食用指南
前端
Cao_Ron23 分钟前
用 ECharts graph 画出卡片式矩形节点
前端
skiyee31 分钟前
换个底座的 Wot Starter 居然让 AI 更懂项目!
前端·ai编程
anyup38 分钟前
这一套绝了!AI 大模型写故事,星云 SDK 驱动 3D 数字人实时演绎
前端·人工智能·aigc
耀耀切克闹灬1 小时前
短链跳转
前端
孟陬1 小时前
高质量全方位的 AI 工具 sse-stuntman — Mock SSE 接口
前端·node.js·openai
Cao_Ron1 小时前
给 ECharts 6 graph 节点叠加角标:从原理到性能优化
前端