• 图片懒加载的实现原理
  • 发布于 2个月前
  • 151 热度
    0 评论
前端发展过程中有许多性能优化的操作,比如防抖、节流和图片懒加载等。在这里我们首先聊聊图片懒加载操作。在最近的618中,我们会经常逛像淘宝和京东等购物平台。那你觉得在淘宝页面中的图片资源是打开页面时就一次性全部加载完了呢,还是在你滚轮滚动到的区域才加载图片呢。

一次性全部加载会导致加载时间长、网络资源消耗大、内存占用率高和发出大量图片请求给服务器带来的巨大压力。所以采用的是只加载当前可见区域的图片,随着用户的滚动,当其他图片进入可见区域时,再进行加载。这种方法就是图片的懒加载。这种方式可以有效地提高页面的响应速度,特别是在图片数量较多或网络条件较差的情况下,可以避免页面加载缓慢或卡顿的现象,提供更好的用户体验。
如下图所示,淘宝页面刚打开时并不是全部加载的。

大致思路
图片懒加载布局逻辑:
1.通过将img标签的src属性值都设置为同一个图片的url,这个图片需要尽可能小。这样即可以为后续要真正加载的图片占位置,也可以让页面布局更快地呈现出来,而不是长时间等待图片加载导致空白。
2.将每一个img标签中真正的图片url存放在一个数据属性当中。当需要加载时将该数据属性内的内容赋值给img标签的src属性。

图片懒加载交互逻辑:
1.首先通过获取用户可视窗口高度、滚轮到最顶端的距离和每个图片到最顶端的距离。
2.通过监听器监听滚轮滚动事件触发懒加载函数。
3.但是一开始就出现在可视窗口的图片需要直接加载,所以需要先运行一次懒加载函数。
4.在懒加载函数里通过判断(图片到最顶端的距离)和(用户可视窗口高度+滚轮到最顶端的距离)的大小判断是否需要加载。

编辑代码
html部分
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/hsossms/20240612/v2_8ec7812750194dbd831babce8806c626@000000_oswg5522709oswg1792oswg1024_img_png?x-oss-process=image/resize,m_mfit,w_600,h_400,limit_0/crop,w_600,h_400,g_center/format,webp" />
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/20190808/v2_1565254363234_img_jpg">
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/20190905/v2_1567641293753_img_png">
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/20190905/v2_1567640518658_img_png">
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/20190905/v2_1567642423719_img_000">
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/20190905/v2_1567642425030_img_000">
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/20190905/v2_1567642425101_img_000">
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/20190905/v2_1567642425061_img_000">
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/20190904/v2_1567591358070_img_jpg">
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/20190905/v2_1567641974410_img_000">
<img src="https://misc.360buyimg.com/mtd/pc/common/img/blank.png"
     data-src="https://img.36krcdn.com/20190905/v2_1567641974454_img_000">
在html部分中有一个重要的步骤。你会发现每一个img标签的src属性值都是一样的,我们可以复制链接到浏览器查看该图片。
该图片是中间的小白点:

该图片的属性如下:

可以看出这个图片是非常小的。在页面打开时,加载的都是这个图片,真正要加载的图片url放置在data-src数据属性里面。最后通过JavaScript部分实现将data-src数据属性的内容赋值给img标签的src属性,然后发送HTTP请求加载真正的图片。
css部分
img {
    display: block;
    margin-bottom: 50px;
    width: 400px;
    height: 400px;
}

body {
    background-color: gray;
}
通过设置img 标签样式,让小图片给真正要放的图片占位置。

JavaScript部分
变量定义:其中imgs是包含页面中所有<img>元素的集合,可以通过索引来访问具体的图像元素;num表示图片数量;n记录被加载的图片数量。
const imgs = document.getElementsByTagName('img');
const num = imgs.length;
let n = 0
在全局一个设置scroll事件监听器,当事件触发后会调用lazyload慢加载函数。
window.addEventListener('scroll', lazyload)
定义一个lazyload慢加载函数:
function lazyload() {
    // 堆代码 duidaima.com
    //可视区域的高度
    let screenHeight = document.documentElement.clientHeight;
    //滚动条距离最顶部的距离,
    let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
    //判断图片是否存在在可视区域内
    for (let i = n; i < num; i++) {
        if (imgs[i].offsetTop > scrollTop + screenHeight) {
            break;
        } else {
            //主动触发下载
            imgs[i].src = imgs[i].getAttribute('data-src');
            //记录已经加载过的图片数量
            n = i + 1;
            if (n === num) {
                //全部加载完毕后移除滚动事件
                window.removeEventListener('scroll', lazyload);
            }
        }
    }
}

首先获取可视窗口的高度screenHeight、滚动条距离最顶部的距离scrollTop和具体图片距离最顶端的距离imgs[i].offsetTop。通过for循环遍历每个img元素。


1.当imgs[i].offsetTop > scrollTop + screenHeight时,也就是说该图片的区域并没有被可视区域覆盖过,所以图片不需要加载,也就没有别的后续操作了。
如果imgs[i].offsetTop <= scrollTop + screenHeight时,说明该图片的区域被可视区域覆盖了,图片需要进行加载。

2.当图片需要加载时,将真正的图片url赋值给src属性。由imgs[i].src = imgs[i].getAttribute('data-src')实现。

3.每次加载一张图片n的值就加一。当n的值等于num的值时,也就是所有图片都加载完成后就不需要scroll事件监听器了,所有通过window.removeEventListener('scroll', lazyload)清除监听器。

4.最后因为首屏内的图片需要直接加载,而不是通过scroll事件监听器实现加载。所有需要调用一次慢加载函数。
//方法一
document.addEventListener('DOMContentLoaded', lazyload)
//方法二
window.addEventListener('load', lazyload)
方法一通过DOMContentLoaded事件监听器触发慢加载函数的速度比方法二通过load事件监听器触发慢加载函数的速度快。所以推荐方法一。

节流优化
因为scroll事件监听器在频繁的滑轮滚动会频繁触发。如果直接在事件处理函数中执行大量复杂的操作,可能会导致性能问题。
所以通过使用节流限制事件触发的频率。
const imgs = document.getElementsByTagName('img');
const num = imgs.length;
//用变量记录节流返回的函数
const throttleLazyLoad = throttle(lazyload, 200);
//滚动事件触发懒加载
window.addEventListener('scroll', throttleLazyLoad)
let n = 0
//首屏加载,DOMContentLoaded事件是DOM加载完成,不包括图片(比load事件快)
document.addEventListener('DOMContentLoaded', lazyload)
//首屏加载,load事件是DOM加载完成,包括图片(比DOMContentLoaded事件慢)
window.addEventListener('load', lazyload)
//懒加载函数
function lazyload(event) {
    //可视区域的高度
    let screenHeight = document.documentElement.clientHeight;
    //滚动条距离最顶部的距离,
    let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
    //判断图片是否存在在可视区域内
    for (let i = n; i < num; i++) {
        if (imgs[i].offsetTop > scrollTop + screenHeight) {
            break;
        } else {
            //主动触发下载
            imgs[i].src = imgs[i].getAttribute('data-src');
            //记录已经加载过的图片数量
            n = i + 1;
            if (n === num) {
                //全部加载完毕后移除滚动事件
                window.removeEventListener('scroll', throttleLazyLoad);
            }
        }
    }
}
//节流函数
function throttle(func, limit) {
    let inThrottle;
    return function () {
        const context = this;
        const args = arguments;
        if (!inThrottle) {
            func.apply(context, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

定义了一个节流函数,它接收要执行的函数 func 和时间间隔限制 limit。内部通过一个变量 inThrottle 来标记当前是否处于节流状态。当执行返回的函数时,先判断如果不在节流状态,就立即执行目标函数,并将 inThrottle 设置为 true,同时使用 setTimeout 在指定时间间隔后将 inThrottle 恢复为 false,从而实现了在规定时间间隔内只执行一次函数的节流效果,避免了频繁触发导致的性能问题。


再次优化
在日常工作时,如果让你选择手搓一个节流函数和直接使用工具库里的函数,你肯定也会和我一样偷懒,选择直接使用工具库里的现成的函数。
首先在HTML中使用以下代码引入 Lodash 库。
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
然后可以删除掉你手搓的节流函数了,使用 Lodash 库里的节流函数。
const imgs = document.getElementsByTagName('img');
const num = imgs.length;
//用变量记录节流返回的函数
const throttleLazyLoad = _.throttle(lazyload, 200);//调用Lodash库里的节流函数
//滚动事件触发懒加载
window.addEventListener('scroll', throttleLazyLoad)
let n = 0
//首屏加载,DOMContentLoaded事件是DOM加载完成,不包括图片(比load事件快)
document.addEventListener('DOMContentLoaded', lazyload)
//首屏加载,load事件是DOM加载完成,包括图片(比DOMContentLoaded事件慢)
window.addEventListener('load', lazyload)
//懒加载函数
function lazyload(event) {
    //可视区域的高度
    let screenHeight = document.documentElement.clientHeight;
    //滚动条距离最顶部的距离,
    let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
    //判断图片是否存在在可视区域内
    for (let i = n; i < num; i++) {
        if (imgs[i].offsetTop > scrollTop + screenHeight) {
            break;
        } else {
            //主动触发下载
            imgs[i].src = imgs[i].getAttribute('data-src');
            //记录已经加载过的图片数量
            n = i + 1;
            if (n === num) {
                //全部加载完毕后移除滚动事件
                window.removeEventListener('scroll', throttleLazyLoad);
            }
        }
    }
}
呈现效果
当所有图片加载完后滚动事件就不会触发慢加载函数了,并且也有节流效果。

首屏的图片立即加载,区域图片在滚动到再加载。

用户评论