Skip to content

图片懒加载

一、实现原理

<img>标签本身有一个loading属性来决定是否执行懒加载,但是实测中发现该属性在Edge浏览器中并不起作用,所以需要通过脚本的方式去实现。

简单来说,还没有出现在用户可见范围内的图像资源不加载

具体来说,通过判断<img>标签所处的坐标是否出现在了网页视口,将<img>标签的src值替换为存放在data-src属性里的真正图像链接

判断<img>标签是否出现在可视范围有两种实现方式:

  1. 监听window.onscroll事件,在回调函数中去对比每个<img>标签的坐标和窗口尺寸
  2. 使用Intersection Observer这个接口,判断<img>标签是否出现在可视范围

二、前置知识

1、浏览器屏幕坐标系

image-20221103101645081

存在两种坐标系

  • 相对于窗口window,从窗口的(顶部,左部)开始计算,坐标变量为**clientXclinetY**
  • 相对于文档document,从文档根的(顶部,左部)开始,坐标变量为**pageXpageY**

页面滚动到起点时,(pageX,pageY)(clientX,clientY)相等

页面发生滚动(即滚动条移动)之后,(pageX,pageY) = (clientX + scrollLeft, clinetY + scrollTop), 其中scrollTop/scrollLeft代表页面节点与文档根(顶部,左部)的滚动距离,可通过window.pageYOffsetdocument.body.scrollTopdocument.documentElement.scrollTop获取

2、获取节点坐标:getBoundingClinetRect

方法 elem.getBoundingClientRect() 返回最小矩形的窗口坐标,该矩形将 elem 作为内建 DOMRect 类的对象。

主要的 DOMRect 属性:

  • x/y —— 矩形原点相对于窗口的 X/Y 坐标
  • width/height —— 矩形的 width/height(可以为负,负值表示右下角作为起点,正常是左上角作为起点)

此外,还有派生(derived)属性:

  • top/bottom —— 顶部/底部矩形边缘的 Y 坐标
  • left/right —— 左/右矩形边缘的 X 坐标

width/height为正数时的示意图如下:

image-20221103104333989

width/height为正数时的示意图如下:

image-20221103104751579

总结:获取节点的坐标

  1. 相对于窗口的坐标 —— elem.getBoundingClientRect()
  2. 相对于文档的坐标 —— elem.getBoundingClientRect() 加上当前页面滚动。

3、获取窗口尺寸

窗口有两种定义,一种是当前窗口的实际大小(可能因为被缩小而变化),另外一种是屏幕的大小(固定值)

第一种,获取当前窗口实际大小,使用window对象

现代浏览器老版浏览器
窗口内高度(除去了菜单栏、工具栏等等)innerHeightdocument.documentElement.clientHeight 或 document.body.clientHeight
窗口内宽度innerWidthdocument.documentElement.clientWidth 或document.body.clientWidth

第二种,获取屏幕固定宽高

screen.width屏幕宽度
screen.height屏幕高度

4、判断节点是否在可视范围

js
function isInView(elem){
    if(elem.getBoundingClientRect().top <= window.innerHeight){
        return true
    }else{
        return false
    }
}

5、Intersection Observer

该接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗 (viewport) 交叉状态的方法

该接口可以方便的检测元素的可视状态,应用场景广泛:

  • 当页面滚动时,懒加载图片或其他内容。
  • 实现“可无限滚动”网站,也就是当用户滚动网页时直接加载更多内容,无需翻页。
  • 对某些元素进行埋点曝光
  • 滚动到相应区域来执行相应动画或其他任务。

构造器

IntersectionObserver(),接受一个回调函数

js
const observer = new IntersectionObserver((entries)=>{
    console.log(entries)
})
let hList = document.querySelectorAll('h3')
observer.observe(hList[0])

输出结果如下:

image-20221103130154181

可以看到enries是一个IntersectionObserverEntry对象数组,该对象的属性如上,其中isIntersecting是一个Boolean值,表示被观察的节点是否与文档视窗交叉(即有没有出现在可见窗口中)

属性说明
IntersectionObserver.root所监听对象的具体祖先元素 (element)。如果未传入值或值为null,则默认使用顶级文档的视窗
方法说明
disconnect()使IntersectionObserver对象停止监听工作
[`observe()开始监听一个目标元素
unobserve()停止监听特定目标元素

用法示例:

html
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <style>
        h3 {
            margin: 200px;
        }
    </style>
</head>
<body>
    <div>
        <h3>测试文本1</h3>
        <h3>测试文本2</h3>
        <h3>测试文本3</h3>
        <h3>测试文本4</h3>
        <h3>测试文本5</h3>
    </div>
    <script>
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    let node = entry.target
                    console.log(node.textContent + ' 出现在了窗口中')
                    observer.unobserve(node)
                }
            })
        })
        let hList = document.querySelectorAll('h3')
        hList.forEach(h => {
            observer.observe(h)
        })
    </script>
</body>
</html>

image-20221103130952810

三、动手实践

第一种方式:监听window.onscroll事件,在回调中判断图片是否出现在窗口可视范围,使用节流优化监听事件

html
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <style>
        img{
            display: block;
            height: 400px;
        }
    </style>
</head>
<body>
    <div>
        <img src="https://tse4-mm.cn.bing.net/th/id/OIP-C.nRlAFygdctTCHmIWN7GxRwHaEK?pid=ImgDet&rs=1" data-src="https://qiuzcc-typora-images.oss-cn-shenzhen.aliyuncs.com/images/2022/202210141603798.JPG" alt="">

        <img src="https://tse4-mm.cn.bing.net/th/id/OIP-C.nRlAFygdctTCHmIWN7GxRwHaEK?pid=ImgDet&rs=1" data-src="https://qiuzcc-typora-images.oss-cn-shenzhen.aliyuncs.com/images/2022/%E9%AA%91%E8%A1%8C.jpg" alt="">

        <img src="https://tse4-mm.cn.bing.net/th/id/OIP-C.nRlAFygdctTCHmIWN7GxRwHaEK?pid=ImgDet&rs=1" data-src="https://qiuzcc-typora-images.oss-cn-shenzhen.aliyuncs.com/images/2022/202210141601847.jpg" alt="">

        <img src="https://tse4-mm.cn.bing.net/th/id/OIP-C.nRlAFygdctTCHmIWN7GxRwHaEK?pid=ImgDet&rs=1" data-src="https://qiuzcc-typora-images.oss-cn-shenzhen.aliyuncs.com/images/2022/202210121140688.jpg" alt="">
        
        <img src="https://tse4-mm.cn.bing.net/th/id/OIP-C.nRlAFygdctTCHmIWN7GxRwHaEK?pid=ImgDet&rs=1" data-src="https://qiuzcc-typora-images.oss-cn-shenzhen.aliyuncs.com/images/2022/202205270028510.png" alt="">
        
    </div>
    <script>
        function throttle(fn,delay){
            let timeoutId;
            return function(){
                let context = this
                let args = arguments
                if(!timeoutId){
                    timeoutId = setTimeout(()=>{
                        timeoutId=null
                        fn.apply(context,args)
                    },delay)
                }
            }
        }
       
        let start = 0   //start需要声明为全局变量,否则每次调用函数start都会重新刷新为0,也就失去了它的意义了
        function imgLazyLoad(){
            console.log('触发imgLazyLoad')
            let imgs = document.querySelectorAll('img')
            for(let i=start;i<imgs.length;i++){
                let img = imgs[i]
                if(img.getBoundingClientRect().top <= window.innerHeight){
                    img.src = img.dataset.src
                    start = i+1
                }
            }
        }
        window.onscroll = throttle(imgLazyLoad,250)
        imgLazyLoad()
    </script>
</body>
</html>

第二种方式:使用IntersectionObserver

js
//替换<script>部分
function imgLazyLoad(imgs) {
    imgs.forEach(event=>{
        if(event.isIntersecting){
            const image = event.target
            image.src = image.dataset.src
            observer.unobserve(image)
            console.log('triger')
        }
    })
}
const observer = new IntersectionObserver(imgLazyLoad)
let imgs = document.querySelectorAll('img')
imgs.forEach(img => {
    observer.observe(img)
})

IntersectionObserver允许你追踪目标元素与其祖先元素或视窗的交叉状态

通过isIntersecting属性来判断是否出现

  • observer.observe添加交叉监听,给每个img添加监听
  • observer.unobserve取消交叉监听,img出现时给src赋值,并取消监听

四、性能提升效果

为了放大对比效果,特意选择了体积较大的图像文件

首先是没有懒加载时的加载时间:一共花了20.7s,五张图片一共19.4M

image-20221103112657840

使用懒加载后,首次只加载前三张图片,一共花了5.03s,三张图片8.4M

image-20221103120837216

Welcome to the site