图片延迟懒加载-从盒模型到新特性再到未来新特新的不同实现

314 阅读3分钟

前言

完整代码,请移步:CQ-engineer/juejinBlog/imgLazyLoad

之前笔者,也了解过图片懒加载,但也仅仅是了解,写过一些简单的代码,但是没有真正用到项目当中,这次正好项目当中用到了,而且结合一些学习资料,那么我就来根据各种方式方法来实现一遍吧

首先看看之前的代码

// 实现懒加载
// <ul>
//   <li><img src="./imgs/default.png" data="./imgs/1.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/2.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/3.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/4.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/5.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/6.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/7.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/8.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/9.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/10.png" alt=""></li>
// </ul>

let imgs =  document.querySelectorAll('img')
// 可视区高度
let clientHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
/*function lazyLoad () {
  // 滚动卷去的高度
  let scrollTop = window.scrollY || window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
  for (let i = 0; i < imgs.length; i ++) {
    // 图片在可视区冒出的高度
    let x = clientHeight + scrollTop - imgs[i].offsetTop
    // 图片在可视区内
    if (x > 0 && x < clientHeight+imgs[i].height) {
      imgs[i].src = imgs[i].getAttribute('data')
    }
  }
}*/
function lazyLoad () {
  imgs.forEach(item=>{
    if(item.getBoundingClientRect().top < clientHeight){
        imgs[i].src = imgs[i].getAttribute('data')
    }
  })
}
setTimeout(lazyLoad, 500)
window.addEventListener('scroll', throttle(lazyLoad, 500));

以上代码简单实现了懒加载的效果,但是太简单了,今天准备讲讲它的前世今生,让大家能更好的理解它,那么现在开始吧😊

从浏览器底层渲染机制分析懒加载的意义

前端性能优化一直提到要各种懒加载,那为什么呢?

  • 浏览器渲染页面过程: 构建DOM Tree --> 构建CSSOM --> 结合生成render Tree --> 绘制

构建DOM树中如果遇到img

老版本:阻碍DOM渲染

新版本:不会阻碍 每一个图片请求都会占用一个HTTP(浏览器同时发送的HTTP 6个,因为http1.1中最多可以并发6个tcp),那么就会耽误其他资源的拉取,回来资源后会和RENDER TREE一起渲染,会让页面第一次加载变慢(白屏)

图片延迟懒加载就是:第一次不请求也不渲染图片,等页面加载完,其他资源都渲染好了,再去请求加载屏幕区图片

效果

瀑布流思路

这里简单说一下瀑布流的实现,首先有四列,将每个columns高度获取到,根据高度来进行瀑布流,数据四个一组,img高度最大的放在高度最小的columns中,高度最小的放在高度最大的columns中,这样就能保证四列各自的高度不会相差太大。详情请看代码中createHTML()

懒加载思路

  • 真实图片地址存放在自定义属性中,比如<img src="" real-src="./images/1.jpg" />
  • 获取到页面中所有img元素,判断是否在可视区域,在就将img.src = img.getAttribute("real-src")
  • 监听scroll事件,可以配合函数节流,查看utils中的throttle
注意:最关键就在于如何去判断是否在可视区域

最初基于JS盒模型实现的懒加载方案

基于JS盒模型就是利用offsetTop、scrollTop来判断 所以我们可以写出这样的代码

function lazyFunc() {
    console.log('ok!')
    if (!itemBoxs) {   //第一次没有就获取,不用每次获取,提高性能
        itemBoxs = Array.from(document.querySelectorAll('.itemBox'));
    }
    itemBoxs.forEach(itemBox => {
        // 已经处理过则不在处理
        let isLoad = itemBox.getAttribute("isLoad");
        if (isLoad) return;
        /* 加载条件:盒子顶边距离BODY距离 < 浏览器视口高度 + scrollTop */
        // let B = utils.offset(itemBox).top + itemBox.offsetHeight,
        let B = utils.offset(itemBox).top,
            A = winH + document.documentElement.scrollTop;
        if (B <= A) {
            lazyImgFunc(itemBox);
        }
    })
}
function lazyImgFunc(itemBox) {
    let img = itemBox.querySelector("img"),
        trueImg = img.getAttribute("real-src");
    img.src = trueImg;
    img.onload = function () {
        // 图片加载成功
        utils.css(img, 'opacity', 1);
    }
    img.removeAttribute("real-src")
    // 记录图片已经处理过了
    itemBox.setAttribute("isLoad", true)
}

基于getBoundingClientRect的进阶方案

getBoundingClientRect用于获取某个元素相对于视窗的位置集合。集合中有top, right, bottom, left等属性。 left: 元素左边距浏览器左边距离

right: 元素右边距浏览器左边距离

top: 元素顶边距浏览器顶边距离

bottom: 元素底边距浏览器顶边距离

那么判断的条件就是img盒子.getBoundingClientRect().top <= 浏览器可视区高,这样是不是就好理解多了

function lazyFunc() {
    console.log('ok!')
    if (!itemBoxs) {   //第一次没有就获取,不用每次获取,提高性能
        itemBoxs = Array.from(document.querySelectorAll('.itemBox'));
    }
    itemBoxs.forEach(itemBox => {
        // 已经处理过则不在处理
        let isLoad = itemBox.getAttribute("isLoad");
        if (isLoad) return;
        /* getBoundingClientRect 
            left: 元素左边距浏览器左边距离
            right: 元素右边距浏览器左边距离
            top: 元素顶边距浏览器顶边距离
            bottom: 元素底边距浏览器顶边距离
        */
        let {
            top,
            bottom
        } = itemBox.getBoundingClientRect();
        if (top <= winH) {
            lazyImgFunc(itemBox);
        }
    })
}
function lazyImgFunc(itemBox) {
    let img = itemBox.querySelector("img"),
        trueImg = img.getAttribute("real-src");
    img.src = trueImg;
    img.onload = function () {
        // 图片加载成功
        utils.css(img, 'opacity', 1);
    }
    img.removeAttribute("real-src")
    // 记录图片已经处理过了
    itemBox.setAttribute("isLoad", true)
}

终极方案:IntersectionObserver

IntersectionObserver类似于事件监听,发布订阅模式,我们可以通过一个简单的例子来了解下

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片懒加载</title>
    <link rel="stylesheet" href="css/reset.min.css">
    <style>
        .box {
            width: 300px;
            margin: 1300px auto;
        }

        .box img {
            width: 100%;
        }
    </style>
</head>
<body>
    <div class="box" id="box">
        <img src="images/1.jpg" alt="">
    </div>
    <script>
        let observer = new IntersectionObserver(changes => {
            // changes包含所有监听对象的信息
            // target当前监听的对象
            // isIntersecting 是否出现在视口中
            // boundingClientRect 
            // ...
            console.log(changes);
            let item = changes[0];
            if (item.isIntersecting) {
                // 进入到视口
                // ...
                observer.unobserve(item.target);
            }
        });
        observer.observe(document.querySelector("#box"));
    </script>
</body>
</html>

当我们监听observer.observe(document.querySelector("#box"))函数就会触发,当元素进入或者离开视口也会触发,可以通过isIntersecting属性来判断是否进入到视口,不用监听scroll事件,做到元素一可见便触发回调。不需要监听scroll,也不需要函数节流,性能比较好。

let observer = new IntersectionObserver(changes => {
    changes.forEach(item => {
        let { target, isIntersecting } = item;
        if (isIntersecting) {
            lazyImgFunc(target)
            observer.unobserve(target)
        }
    })
})
function lazyFunc() {
    console.log('ok!')
    if (!itemBoxs) {   //第一次没有就获取,不用每次获取,提高性能
        itemBoxs = Array.from(document.querySelectorAll('.itemBox'));
    }
    itemBoxs.forEach(itemBox => {
        observer.observe(itemBox)
    })
}
function lazyImgFunc(itemBox) {
    let img = itemBox.querySelector("img"),
        trueImg = img.getAttribute("real-src");
    img.src = trueImg;
    img.onload = function () {
        // 图片加载成功
        utils.css(img, 'opacity', 1);
    }
    img.removeAttribute("real-src")
}

未来设想:img.loading=lazy

现在chrome 76已经 支持了,可以通过loading="lazy"来实现

<img src="./images/1.jpg" alt="" loading="lazy">