从原理上理解性能优化之图片加载机制(懒加载/预加载)

697 阅读17分钟

前言

每一个技术的诞生都有它的理由,遇见了什么问题需要它解决,它解决了什么问题。

今天聊的是前端页面上图片展示问题(用户可能看到图片区域空白或者在逐渐完整的展示,造成用户体验感差)及其解决方法。

因为我们无法改变客户端的宽度网速等和服务器的负载等诸多影响图片加载速度的元素,但是我们可以优化图片的加载机制(预加载、懒加载等)。

长页面图片展示问题

在前端项目开发过程中,常出现页面很长(要滚动看下面的内容),且图片较多的情况。现在来看一个例子,使用的是原生语言开发的一个PC官网的首页(HTML+CSS+JavaScript,后面补上哦~),且为了方便开发和打包,使用了Gulp构建工具(因为页面也不包,既简单又简洁)。

优化图片加载方式

注意:记得给图片设置宽度和高度~

懒加载 Lazyload

懒加载,延迟加载相应的资源。图片懒加载就是延迟加载图片,不再是在页面渲染期间加载全部的图片(易造成用户体验差和服务器压力大的问题),而是只有当图片出现在可视区域时加载(页面滚动期间图片出现在可视区域)。图片懒加载适用于页面很长且图片较多的页面。

懒加载实现原理

懒加载实现原理 改造页面上的img标签,将真实图片地址从src改到data-src,然后监听页面滚动,只有在图片出现在可视区域时再去加载。

正常情况下:在程序执行到img标签时,读到src便会向服务器请求对应图片资源:

<div class="container">
    <img src="public/images/img-1.jpg" alt="" />
</div>

实现图片懒加载分以下两步:

  • 第一步:将img标签的真正的地址设置到data-src上,刷新页面,Network请求列表里便没有对该图的加载(因为img标签里src值是空的):
<div class="container">
    <img data-src="public/images/img-1.jpg" alt="" />
</div>
  • 第二步:在脚本里控制图片加载时机,调用函数lazyload实现图片加载:将img标签的data-src属性值设置到src(即data-src->src,也就向服务器发起资源请求):
/**
 * 实现图片懒加载
 */
function lazyload() {
    const imgTagList = document.getElementsByTagName("img")
    // ==因为 document.getElementsByTagName("img") 返回的是一个类数组(HTMLCollection), 所以需要将类数组转为数组以遍历
    // console.log("imgTagList类型:", Object.prototype.toString(imgTagList))
    // ==类数组转数组两种方式:Array.from(类数组);三点展开符[...类数组]==
    Array.from(imgTagList).forEach(imgDom => {
        // ==获取dom属性方式==
        // dom.getAttribute(属性名称);如果是以data-开头命名的属性可使用dom.dataset.属性名
        imgDom.setAttribute("src", imgDom.dataset.src)
    })
}

window.onload = ()=> {
    // ==加个定时器好看图片加载效果(Network)==
    setTimeout(() => {
        // ==懒加载==
        lazyload()
    }, 2000);
}

通过上述两步,图片便可延迟加载(在浏览器Network看请求效果哦)~

给懒加载的图片增加类名lazy

因为后面还有预加载,把懒加载的图片加上类名lazy,以区分~

<div class="container">
    <img class="lazy" data-src="public/images/img-1.jpg" alt="" />
</div>

同步调整lazyload函数:

/**
 * 实现图片懒加载
 */
function lazyload() {
    // ==获取页面上需要懒加载的图片列表==
    const lazyImgList = document.querySelectorAll("img.lazy") // NodeList
    // ==data-src->src==
    Array.from(lazyImgList).forEach((imgDom)=> {
        // ==当前src(可能是预加载的占位的底图)==
        const curSrc = imgDom.getAttribute("src")
        // ==真实src==
        const realSrc = imgDom.dataset.src
        // ==避免重复设置判断==
        if (curSrc !== realSrc) {
            imgDom.setAttribute("src", realSrc)
        }
    })
}

页面滚动时只加载可视区域的图片

对于一个很长且有很多图片的页面,若是在页面渲染期间将所有的图片加载出来,而用户只能看到可视区域的图片,这样会造成极大的性能浪费(宽度流量、服务器压力等),而且在图片加载期间可能造成空白展示等诸多问题引起用户体验差等。在这种页面中,就很适合使用按需加载,只有在图片出现在可视区域才加载(当然也可以稍微提前一丢丢~)。

增加对页面滚动的监听事件,对未加载的图片进行计算,若出现在可视区域,则加载,否则不加载。

计算过程中涉及的高度值:页面的可视区域的高度winViewPortHeight,页面纵向滚动距离scrollTop和图片距离滚动节点顶部的偏移量offsetTop

判断是否加载图片:如果offsetTop小于winViewPortHeight+scrollTop,即在可视区域,加载图片;否则不加载。

修改函数脚本,增加外部函数lazyloadImageList,定义公共变量及控制事件执行等以实现图片加载,具体代码如下:

/**
 * 实现图片懒加载
 */
function lazyloadImageList() {
    // ==事件监听对象==
    const watchDom = window
    // ==监听事件类型==
    const watcEventName = "scroll"
    // ==获取可视区域的高度==
    // ==window.innerHeigh:浏览器窗口的视口高度;document.documentElement.clientHeight:文档根元素的可见高度;两者值相同==
    // console.log(document.documentElement.clientHeight, window.innerHeight)
    const winViewPortHeight = window.innerHeight || document.documentElement.clientHeight || 0

    // ==获取页面上需要懒加载的图片列表==
    const lazyImgList = Array.from(document.querySelectorAll("img.lazy"))
        .map((dom,id)=> ({id, dom, lazy:false})) // NodeList->Array
    // ==需要加载的图片数量==
    const lazyImgLength = lazyImgList.length
    // ==记录已加载图片的数量==
    let lazyedImgCount = 0

    // ==懒加载==
    const lazyload = ()=> {
        // ==如果图片全部加载完毕,移除监听器==
        ...
        // ==如果没有,则计算是否出现在可视区域==
        ...
    }

    // ==首次首屏加载==
    lazyload()
    // ==监听滚动以加载可视区域的图片==
    // ==性能优化:使用节流控制事件发生频率==
    addEventListen(watchDom, watcEventName, throttle(lazyload, 100))
}

内部函数lazyload负责实现图片的加载。过滤掉已加载的图片(若imgNode.lazy为true则表示已加载),计算未加载的图片是否出现在可视区域,curImgOffsetTop < winViewPortHeight + scrollTop。 这里还增加了一个对未加载的img标签的当前展示的图片src地址和真实地址data-src的值是否相等的条件判断,因为一般情况下,会使用预加载的底图来展示着,以避免图片加载出来之前是一片空白。 等图片加载完成后,将imgNode.lazy置为true。 具体实现如下:

// ==懒加载==
const lazyload = ()=> {
    // ===剔除掉已加载的图片=
    const curLazyImgList = lazyImgList.filter(imgNode => !imgNode.lazy)
    // ==是否还需加载判断==
    if (!curLazyImgList.length && lazyedImgCount >= lazyImgLength) {
        // ==移除监听事件==
        removeEventListen(watchDom, watcEventName, ()=> {
            console.log("需要懒加载的图片已全部加载完毕!")
        })
        return
    }
    // ==当前滚动的高度==
    const scrollTop = watchDom === window 
        ? (document.body.scrollTop || document.documentElement.scrollTop || 0) 
        : watchDom.scrollTop || 0 // 注意:若没有滚动则scrollTop值为undefined,所以需要置为0
    // ==data-src->src==
    curLazyImgList.forEach((imgNode) => {
            const imgDom = imgNode.dom
            // ==当前图片的滚动高度==
            const curImgOffsetTop = imgDom.offsetTop
            // ==当前src(可能是预加载的占位的底图)==
            const curSrc = imgDom.getAttribute("src")
            // ==真实src==
            const realSrc = imgDom.dataset.src
            // ==是否出现在可视区域+避免重复设置判断==
            if ((curImgOffsetTop < winViewPortHeight + scrollTop) && curSrc !== realSrc) {
                // ==设置src为真实的地址==
                imgDom.setAttribute("src", realSrc)
                // ==更新已加载标识==
                imgNode.lazy = true
                // ==已加载图片+1==
                lazyedImgCount++
            }
    })
}

对应html代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport"
        content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
    <meta name="author" content="露水晰123,1542249206@qq.com" />
    <meta name="keywords" content="懒加载,预加载,性能优化,JavaScript,gulp" />
    <meta name="description" content="前端性能优化之图片加载" />
    <title>LearnLoadImage页面</title>
    <link rel="stylesheet" href="assets/styles/learnLoadImageDefault.css" />
</head>
<body>
    <div class="container">
        <img class="lazy" data-src="public/images/img-1.jpg" alt="图片正在加载中..." />
        <img class="lazy" data-src="public/images/img-2.jpg" alt="图片正在加载中..." />
        <img class="lazy" data-src="public/images/img-3.jpg" alt="图片正在加载中..." />
        <img class="lazy" data-src="public/images/img-4.jpg" alt="图片正在加载中..." />
        <img class="lazy" data-src="public/images/img-5.jpg" alt="图片正在加载中..." />
        <img class="lazy" data-src="public/images/img-6.jpg" alt="图片正在加载中..." />
        <img class="lazy" data-src="public/images/img-7.jpg" alt="图片正在加载中..." />
        <img class="lazy" data-src="public/images/img-8.jpg" alt="图片正在加载中..." />
        <img class="lazy" data-src="public/images/img-9.jpg" alt="图片正在加载中..." />
        <img class="lazy" data-src="public/images/img-10.jpg" alt="图片正在加载中..." />
    </div>
    <script src="assets/js/learnLoadImageDefault.js"></script>
</body>
</html>

至此已实现图片的延迟加载以及只加载可视区域的图片~

若是想要一些动画的效果,需要调整下代码。设置样式:

img.lazy {
    display: block;
    width: 100%;
    height: 500px;
    text-align: center;
    padding: 20px;
    box-sizing: border-box;
    /* 给图片的延迟加载增加淡入的动画效果 */
    opacity: 1;
    transition: opacity .3s ease-in;
}
img.lazy[data-src] {
    opacity: 0;
}

再修改函数lazyload,将真实图片地址赋值给src后移除data-src属性,代码修改:

// ==设置src为真实的地址==
imgDom.setAttribute("src", realSrc)
// ==新增:真实图片加载完成后移除data-src,配合样式实现图片的淡入淡出效果==
imgDom.removeAttribute("data-src")

这样便配合样式实现了图片的淡入效果,具体效果可根据需求决定哦~

预加载 Preload

有懒加载就有相对应的预加载,预加载指提前加载(preload)。图片预加载指的是将图片提前缓存到本地,用的时候直接从缓存中拿,节省了请求的时间,增加用户体验感(若是在用的时候再去加载可能出现空白,用户体验差)。

预加载实现方法有两种形式:一种是通过link标签设置rel="preload"预加载静态资源,一种是通过Image对象来实现图片的预加载。

通过link标签预加载静态资源

link预加载的好处:让浏览器提前加载指定资源(这里预加载完成后并不执行),在需要执行的时候再执行,这样将加载和执行分开,可以不阻塞渲染和window.onload事件。

提前预加载指定资源,特别是字体文件,不会再出现font字体在页面渲染出来后,才加载完毕,然后页面字体闪一下变成预期字体。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport"
        content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
    <meta name="author" content="露水晰123,1542249206@qq.com" />
    <meta name="keywords" content="懒加载,预加载,性能优化,JavaScript,gulp" />
    <meta name="description" content="前端性能优化之图片加载" />
    <title>LearnLoadImage页面</title>
    <link rel="stylesheet" href="assets/styles/learnLoadImageDefault.css" />
    
    <!-- 图片预加载,可以预加载底图 -->
    <!-- 还可以加上媒体查询,根据屏幕尺寸来加载相应的图(响应式加载) -->
    <link rel="preload" as="image" href="public/images/slide-min.jpg" media="(max-width:600px)" />
    <link rel="preload" as="image" href="public/images/slide-max.jpg" media="(min-width:601px)" />

    <!-- 样式预加载 -->
    <link rel="preload" as="style" href="public/styles/index.css" />

    <!-- 字体预加载(常用到) crossorigin允许跨域请求获取资源 type属性可以确保浏览器只获取自己支持的资源 -->
    <link rel="preload" as="font" type="font/woff2" href="public/fonts/font.woff2"  crossorigin />

    <!-- 脚本预加载(不会阻塞window的onload事件,async会阻塞window.onload事件) -->
    <link rel="preload" as="script" href="public/js/jquery.min.js" />
</head>
<body>
    <div class="container">
        <img src="public/images/img-1.jpg" data-src="public/images/img-2.jpg" alt="图片正在加载中..." />
    </div>
    <script src="assets/js/learnLoadImageDefault.js"></script>
</body>
</html>

通过Image对象来实现图片的预加载

Image对象是JavaScript中的宿主(或内置)对象,代表嵌入的图像。当我们创建一个Image对象时,就相当于给浏览器缓存了一张图片,Image对象也常用来做预加载图片(也就是将图片预先加载到浏览器中,当浏览图片的时候就能享受到极快的加载速度)。在HTML页面中,<img>标签每出现一次,也就创建了一个Image对象。

在创建Image对象后,如果没有给它的width和height属性赋值,那它的width和height的默认值都为0。

// ==预加载==
function preloadImage() {
    // ==创建一个Image对象==
    const image = new Image()
    // ==定义Image对象的src,表示向服务器请求该图片资源到本地(后面再用便是从缓存中拿,节省了请求时间)==
    image.src = "public/images/img-1.jpg"
}

下面实现图片预加载,为了测试Image对象是否向服务器请求了资源,将html文文件中的图片src设置到data-src即不加载图片:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport"
        content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
    <meta name="author" content="露水晰123,1542249206@qq.com" />
    <meta name="keywords" content="懒加载,预加载,性能优化,JavaScript,gulp" />
    <meta name="description" content="前端性能优化之图片加载" />
    <title>LearnLoadImage页面</title>
    <link rel="stylesheet" href="assets/styles/learnLoadImageDefault.css" />
</head>
<body>
    <div class="container">
        <img data-src="public/images/img-1.jpg" alt="图片正在加载中..." />
    </div>
    <script src="assets/js/learnLoadImageDefault.js"></script>
</body>
</html>ava

为方便测试,在页面加载完毕后,设置个定时器执行预加载函数preloadImage

// ==方便测试,设置定时器==
window.onload = ()=> {
    setTimeout(() => {
        // ==预加载==
        preloadImage()
    }, 1000);
}

打开浏览器的Network看图片请求效果~

给图片预加载函数preloadImage增加回调配置

Image对象有三个事件(img标签也有):

  • onload: 表示图片加载完后执行的回调(异步);
  • onerror: 图片加载失败后执行的回调(异步);
  • onabort: 图片加载期间,用户通过点击停止加载(浏览器上的红色叉叉)时触发,通常在这里触发一个提示(图片正在加载中...);
// ==预加载==
function preloadImage() {
    // ==创建Image对象==
    const image = new Image()

    // ==设置方法==
    // ==图片加载成功后的回调==
    image.onload = (data)=> {
        console.log("1:图片加载成功", data)
    }
    // ==图片加载失败后的回调==
    image.onerror = (data) => {
        console.log("2:图片加载失败", data)
    }
    // ==图片加载期间用户点击的回调==
    image.onabort = (data) => {
        console.log("3:图片正则加载中", data)
    }

    // ==设置Image对象的src,表示向服务器请求图片资源==
    // ==因为在IE浏览器下,给Image对象的src赋值须在image.onload方法之后,所以将该行代码放在下面==
    image.src = "public/images/img-1.jpg"
    // ==测试图片加载失败回调==
    // image.src = "xxx"
    
    // ==测试图片加载回调是同步还是异步(证明:异步)==
    console.log(0)
}
  • 图片加载成功,触发image.onload事件,终端打印如下:
0
1:图片加载成功 Event {isTrusted: true, type: 'load', target: img, currentTarget: img, eventPhase: 2, …}isTrusted: truebubbles: falsecancelBubble: falsecancelable: falsecomposed: falsecurrentTarget: nulldefaultPrevented: falseeventPhase: 0returnValue: truesrcElement: nulltarget: nulltimeStamp: 1163.2000000476837type: "load"[[Prototype]]: Event
  • 图片加载失败(将image.src值修改为xxx),触发image.onerror事件,终端打印如下:
0
2:图片加载失败 Event {isTrusted: true, type: 'error', target: img, currentTarget: img, eventPhase: 2, …}

上述说明,Image对象的三个事件中onloadonerror是异步的~

进阶:想要在图片预加载后再执行某些操作

上面简单的实现通过Image对象控制图片的加载时机,但是我想要在图片加载成功后执行某些操作,怎么做呢?

想到使用ES6的Promise,若预加载图片加载成功后返回一个Promise对象,这样便可以调用then方法执行图片加载后要执行的操作了。而且如果预加载图片较多,还可以使用Promise.all方法保证图片的加载全部执行结束。下面来看下吧~

为了同预加载函数preloadImage区分, 增加函数preloadImagePromise,其代码如下:

// ==预加载图片:返回Promise对象==
function preloadImagePromise(imageSrc) {
    // ==创建一个Image对象==
    const image = new Image()

    // ==返回一个Promise对象==
    return new Promise((resolve, reject)=> {
        // ==Promise的构造函数:立即执行==
        image.onload = (data)=> {
            console.log(`1:图片${imageSrc}加载成功`, data)
            // ==图片加载成功后==
            resolve(data) // 更改Promise的状态:pending->fulfilled
        }
        image.onerror = (data) => {
            console.log(`2:图片${imageSrc}加载失败`, data)
            // ==图片加载失败后==
            reject(data) // 更改Promise的状态:pending->rejected
        }
        image.onabort = (data) => {
            console.log(`3:图片${imageSrc}正在加载中`, data)
        }
        // ==设置Image对象的src==
        image.src = imageSrc
        // ==测试图片加载失败==
        // image.src = "xxx"
    })
}

在页面加载完再调用preloadImagePromise

window.onload = ()=> {
    // ==加个定时器好看图片加载效果(Network)==
    setTimeout(() => {
        // ==预加载==
        // preloadImage()
        // ==图片地址==
        const imageSrc = "public/images/img-1.jpg"
        preloadImagePromise(imageSrc).then(data=> {
            console.log("4:图片预加载成功", data)
        }).catch(data=> {
            console.log("5:图片预加载失败", data)
        })
    }, 1000);
}

执行效果:

  • 图片加载成功,执行then回调函数,终端打印如下:
1:图片public/images/img-1.jpg加载成功 Event {isTrusted: true, type: 'load', target: img, currentTarget: img, eventPhase: 2, …}
4:图片预加载成功 Event {isTrusted: true, type: 'load', target: img, currentTarget: img, eventPhase: 2, …}
  • 将image.src值设置为xxx,导致图片加载失败, 通过catch方法捕获错误,终端打印如下:
2:图片public/images/img-1.jpg加载失败 Event {isTrusted: true, type: 'error', target: img, currentTarget: img, eventPhase: 2, …}
5:图片预加载失败 Event {isTrusted: true, type: 'error', target: img, currentTarget: img, eventPhase: 2, …}

至此便是实现了需求:对单张图片预加载完成后在then方法里可执行一些其他操作了~

现在进一步调整需求:预加载的图片有多张,让这些图片全部加载完毕执行某些操作。

继续改造,将函数preloadImagePromise中的resolve(data)reject(data) 修改如下:

image.onload = (data) => {
    const message = `1:图片${imageSrc}加载成功`
    // console.log(message, data) // 去掉打印信息,在catch中捕获
    // ==图片加载成功后==
    resolve({message,data}) // 更改Promise的状态:pending->fulfilled
}
image.onerror = (data) => {
    const message = `2:图片${imageSrc}加载失败`
    // console.log(message, data)
    // ==图片加载失败后==
    reject({message,data}) // 更改Promise的状态:pending->rejected
}

增加预加载图片且返回Promise对象的列表调用函数preloadImageListPromise,控制图片的预加载:

// ==实现对多张图片的预加载及回调==
function preloadImageListPromise() {
    // ==预加载的图片列表==
    const preloadImageList = [
        "public/images/img-1.jpg",
        "public/images/img-2.jpg",
        "public/images/img-3.jpg",
        "public/images/img-4.jpg",
    ]
    // ==预加载图片的回调(也可以将此Promise返回,在外部处理,根据需求选择)==
    // ==Promise.all(Promise对象组成的数组):全部成功后触发then;只要有一个执行失败便返回触发catch==
    // ==Promise.all:成功返回数组且子项的顺序是一致的(不管子项返回的早还是晚);失败返回失败那条对应的数据且不再继续往下执行直接退出==
    // ==Promise.all:并行执行里面所有的Promise对象==
    Promise.all(preloadImageList.map((imageNode => preloadImagePromise(imageNode))))
        .then(data=> {
            console.log("预加载的图片全部加载成功", data)
        }).catch(data=> {
            console.log("预加载图片失败", data)
        })
}

且修改调用方式:

window.onload = ()=> {
    // ==加个定时器好看图片加载效果(Network)==
    setTimeout(() => {
        // ==懒加载==
        // lazyload()
        // lazyloadImageList()
        
        // ==预加载==
        // preloadImage()
        // ==图片地址==
        // const imageSrc = "public/images/img-1.jpg"
        // preloadImagePromise(imageSrc).then(data=> {
        //     console.log("4:图片预加载成功", data)
        // }).catch(data=> {
        //     console.log("5:图片预加载失败", data)
        // })
        // ==新==
        preloadImageListPromise()
    }, 1000);
}

刷新页面,看控制台打印信息(及Network图片请求)~

  • 全部成功,触发then,终端打印如下:
预加载的图片全部加载成功 (4) [{…}, {…}, {…}, {…}]0: {message: '1:图片public/images/img-1.jpg加载成功', data: Event}1: {message: '1:图片public/images/img-2.jpg加载成功', data: Event}2: {message: '1:图片public/images/img-3.jpg加载成功', data: Event}3: {message: '1:图片public/images/img-4.jpg加载成功', data: Event}length: 4[[Prototype]]: Array(0)
  • 若将preloadImagePromise 中设置Image对象的src值直改为xxx, 因为Promise.all是并行执行里面所有的Promise对象,所以若有加载失败,则返回第一个加载失败的(即失败返回是无序的,有失败便结束,谁先失败便返回谁),触发catch。终端打印如下:
预加载图片失败 {message: '2:2:图片public/images/img-3.jpg加载失败', data: Event}

懒加载+预加载

实际开发中,常二者结合使用。

预加载先将图片加载过程中要展示的底图缓存到本地,在页面图片展示区域先展示底图(底图可以是一个loading状态的gif,也可以是一张图片,此类图尽量进行压缩以减少预加载的时间)。当真正的图片出现在可视区域时再使用懒加载

常见的还有一种情况是,图片展示过程是由低质量且模糊的图到高清展示,这个模糊的缩略图是底图(使用预加载),高清图是真正的图(使用懒加载)。

00511bba4b6a5d1bb47e3849d899a7f.jpg

总结

懒加载图片

理念:延迟加载图片或按需加载,只有在图片出现在可视区域才去加载(如果判断图片是否出现在可视区域,需要监听页面滚动事件,进行计算, 当curImgOffsetTop < winViewPortHeight + scrollTop时即在可视区域)。

原理:不再直接给img标签设置src属性为真实的图片地址,而是使用data-src来存储,在未加载的图片出现在可视区域时再将data-src对应的值赋值给src,便实现图片的延迟加载。兼容性好

时机:当然,延迟加载的时机可以根据需求调整,不一定是监听页面滚动事件再进行计算。例如,在通过控制图片的opciacy来实现轮播图效果时,可以在点击图片按钮(上一个/下一个/指定某一个)来触发加载未加载的图片~

预加载图片

理念:预先加载图片,将图片资源提前缓存到本地(内存或硬盘),在用的时候再从缓存中拿(from memory cache OR from disk cache),节省页面上的加载时间和提高用户的体验感。

原理:预加载有两种实现方式,一种是通过link标签且将rel设置preload(表示该资源要预先加载),常用于预加载底图和字体等;一种是通过Image对象提前将图片缓存到本地,在用到的时候直接从缓存中拿。兼容性好

时机:预加载时机可根据需求控制~

img标签上对应的三个事件

<!-- 处理图片加载失败情况 -->
<img 
    src="image/1.jpg" 
    width="258" height="178" 
    οnerrοr="this.src='images/isets.jpg'" 
/>

<!-- 故意把图片的路径写错来测试:
	图片加载失败触发onerror,onerror给img设置了一个默认的加载失败的图片,若该图片也加载失败,则再次触发onerror,导致循环触发onerror,在IE下就会弹出stack overflow的弹框,在谷歌浏览器里会出现频闪(若设置了alt) -->
<img 
    src="xxx" 
    alt="图片2正在加载中..." 
    onerror="this.src='xxx'" 
/>

<!-- 针对上面的问题的处理:在重新给img的src属性赋值时,先将onerror事件清除掉,再赋值,这样就不存在循环调用的问题了,代码如下: -->
<img 
    src="public/images/slide-2.1.jpg" 
    alt="图片2正在加载中..." 
    onerror="this.onerror='';this.src='public/images/slide-2.12.jpg'" 
/>

插件

当然,现在项目大都使用Vue或React等框架开发,相应的也有懒加载插件,如 vue-lazyload 通过在img标签上使用指令v-lazy便可实现懒加载。

  • 安装 vue-lazyload:pnpm i vue-lazyload
  • 在项目入口文件 index.js 中配置:
// ==新增:图片懒加载插件==
import VueLazyload from "vue-lazyload"
// ==新增:初始化懒加载==
app.use(VueLazyload, {
    preLoad: 1.3,
    // error: errorimage, // 图片加载失败展示的图片
    // loading: loadimage, // 图片加载中展示的图片
    attempt: 1
})
  • 注册完就可以在组件中使用了,在img标签上将data-src修改为v-lazy
<template>
    <h3>实现图片的懒加载</h3>
    <div class="container">
        <img v-lazy="'public/images/img-1.jpg'" />
    </div>
</template>

源代码

代码已放入Github链接~

参考