前端性能优化——图片懒加载

1,288 阅读4分钟

性能优化——图片懒加载

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

一、实现原理

<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 = clientX + scrollLeftpageY = clientY + scrollTop

其中scrollTop/scrollLeft代表页面节点与文档根(顶部,左部)的滚动距离

// 通过以下3种方式都可以获取到scrollTop(scrollLeft同理)
window.pageYOffset
document.body.scrollTop
document.documentElement.scrollTop

2、getBoundingClientRect()

该方法返回DOM节点的矩形盒子的属性对象,该矩形将 elem 作为内建 DOMRect 类的对象。这个对象有许多属性,

DOMRect {
    x: 236.5,       // 元素盒子左边界 与 窗口左边界的距离
    y: 900,         // 元素盒子上边界 与 窗口上边界的距离(可视窗口,不包括浏览器的工具栏等),负值表示位于窗口之上
    width: 300,     // 元素盒子宽度
    height: 300,    // 元素盒子高度
    top: 900// 同 y
    bottom: 1200,   // top + height
    left: 236.5,    // 同 x
    right: 536.5,   // left + width
}

y属性的值 小于 window.innerHeight(可视窗口的高度)时,意味着元素进入了可视范围内


3、获取窗口尺寸

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

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

// 窗口高度(后面2种适用于老版浏览器)
window.innerHeight
document.body.clientHeight
document.documentElement.clientHeight// 窗口宽度
window.innerWidth
document.body.clientWidth
document.documentElement.clientWidth

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

// 屏幕宽度(屏幕是硬件设备,宽高都是固定的)
screen.width// 屏幕高度
screen.height

4、Intersection Observer

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

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

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

构造器

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

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()停止监听特定目标元素

用法示例:

<!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



三、动手实践

1.方式一:监听onscroll +节流

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

完整文件源码:study/img_lazy_load_1.html at master · Qiuzcc/study · GitHub

核心代码:

<!DOCTYPE html>
<html lang="en">
<body>
    <img src="https://qiuzcc-typora-images.oss-cn-shenzhen.aliyuncs.com/images/empty_box-Large.jpg"
        data-src="https://qiuzcc-typora-images.oss-cn-shenzhen.aliyuncs.com/images/IMG_3995.JPG" alt="">
    
    <script>
        //节流函数,定时器版本,间隔interval时间执行一次fn函数,触发事件那一刻不执行,事件结束后还会执行一次
         function throttle(fn, interval){...}
         let imgs = document.getElementsByTagName('img')
         let curLoadImgIndex = 0
         function imgLazyLoad() {
             for (let i = curLoadImgIndex; i < imgs.length; i++) {
                 let img = imgs[i]
                 if (img.getBoundingClientRect().top <= window.innerHeight) {
                     img.src = img.dataset.src
                     img.dataset.src = null
                     curLoadImgIndex = i + 1
                 }
             }
         }
                                         
        // 监听滚动事件 + 节流(页面滚动过程中,每0.25秒执行一次回调函数)
        window.onscroll = throttle(imgLazyLoad, 250)
​
        // 立即执行,保证首屏的图片能够正确被加载
        imgLazyLoad()
    </script>
</body>
</html>

2.方式二:IntersectionObserver

完整文件代码:study/img_lazy_load_2.html at master · Qiuzcc/study · GitHub

核心代码:

<script>
    const imgs = Array.from(document.getElementsByTagName('img'))
    const imgLazyLoad = (imgs) => {
        imgs.forEach(item => {
            if (item.isIntersecting) {
                const img = item.target
                img.src = img.dataset.src
                img.dataset.src = null
                observer.unobserve(img)
            }
        })
    }
    const observer = new IntersectionObserver(imgLazyLoad)
    imgs.forEach(img => { observer.observe(img) })
</script>

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

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

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


参考链接

【译】浏览器的坐标系统 · YAPN (gitbooks.io)

坐标 (javascript.info)

Intersection Observer - Web API 接口参考 | MDN (mozilla.org)