彻底玩转图片懒加载及底层实现原理

2,687 阅读6分钟

前言

图片懒加载其实已经是一个近乎“烂大街”的词语了,在大大小小的面试中也会被频繁的问到,我在之前的面试中也被问到了图片懒加载的原因、实现方式及底层原理,但由于自己平时很少做“图片”相关的处理,对于“懒加载”也是知之甚少,所以在面试中问答的也不是很好。

今天,我将首先从浏览器底层渲染机制来剖析为什么要去做图片懒加载,之后我将带大家一起来看下目前主流的几种实现图片懒加载的方式及其实现原理,最后会做一个展望。

为什么要做图片懒加载

要问答这个问题,首先我们先来看下浏览器的底层渲染机制:

1、构建 DOM 树

2、样式计算

3、布局阶段

4、分层

5、绘制

6、分块

7、光栅化

8、合成

而在构建DOM的过程中如果遇到img在新老版本的chrome中表现又是不一样的:

  • 老版本:阻塞 DOM 渲染
  • 新版本:虽然不会阻塞 DOM 渲染,但每一个图片请求都会占用一个 HTTP,而且 Chrome 最多允许对同一个 Host 同时建立六个 TCP 连接

当你打开一个网站时,浏览器会做许多工作,这其中包括下载各种可能用到的资源,然后渲染呈现在你面前,假设你的网站有大量的图片,那么加载的过程是很耗时的,尤其像那些电商类需要大量图片的网站,可想而知,网站的初始加载时间会很长,再加上网络等其它影响,用户体验会很差。

相信你经常遇到过一个网站卡在某个地方,一直在加载,这种体验很不好。我们都希望一输入网址,页面立马就呈现在眼前。

总结一下就是:直接全部加载的话会减缓渲染速度,产生白屏等进而影响用户体验

基于原生 js 实现图片懒加载

相关 API

先来看几个后面会用到的API

document.documentElement.clientHeight

获取屏幕可视区域的高度。

图片来源MDN

element.offsetTop

获取元素相对于文档顶部的高度。

图片来源阮一峰博客

document.documentElement.scrollTop

获取浏览器窗口顶部与文档顶部之间的距离,也就是滚动条滚动的距离。

图片来源Seven's Blog

思路分析

通过上面三个 API,我们获得了三个值:可视区域的高度、元素相对于其父元素容器顶部的距离、浏览器窗口顶部与容器元素顶部的距离也就是滚动条滚动的高度。

虽然这几个API很简单,但是单纯的去说还是有点抽象,这里我们还是用图来展示一下:

看完上面这张图片,我想你已经明白了:如果满足offsetTop-scroolTop<clientHeight,则图片进入了可视区内,我们就去请求进入可视区域的图片。

代码实现

基于上面的分析,我们很容易就可以写出如下代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>基于原生 js 实现图片懒加载</title>
    <style>
        img {
            display: block;
            width: 100%;
            height: 300px;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <img data-src="./img/1.png" alt="">
    <img data-src="./img/2.png" alt="">
    <img data-src="./img/3.png" alt="">
    <img data-src="./img/4.png" alt="">
    <img data-src="./img/5.png" alt="">
    <img data-src="./img/6.png" alt="">
    <img data-src="./img/7.png" alt="">
    <img data-src="./img/8.png" alt="">
</body>
<script>
        var imgs = document.querySelectorAll('img');

        //offsetTop是元素与offsetParent的距离,循环获取直到页面顶部
        function getRealTop(e) {
            var realTop = e.offsetTop;
            while(e = e.offsetParent) {
                realTop += e.offsetTop;
            }
            return realTop;
        }

        function lazyLoad(imgs) {
            var H = document.documentElement.clientHeight;//获取可视区域高度
            var S = document.documentElement.scrollTop || document.body.scrollTop;
            for (var i = 0; i < imgs.length; i++) {
                if (H + S > getRealTop(imgs[i])) {
                    imgs[i].src = imgs[i].getAttribute('data-src');
                }
            }
        }

        window.onload = window.onscroll = function () { //onscroll()在滚动条滚动的时候触发
            lazyLoad(imgs);
        }
</script>
</html>

但上面的代码如果你在lazyLoad中打印,你会发现滚动条上下滚动时,lazyLoad会被频繁调用,造成很大的性能损失,这里我们可以给事件加上节流throttle

基于 getBoundingClientRect()实现图片懒加载

先来了解一下这个API吧:

getBoundingClientRect()用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。getBoundingClientRect()DOM元素到浏览器可视范围的距离(不包含页面看不见的部分)。

该函数返回一个rectObject对象,该对象有 6 个属性:top, left, bottom, right, width, height;这里的topleftcss中的理解很相似,widthheight是元素自身的宽高,但是rightbottomcss中的理解有点不一样。right是指元素右边界距窗口最左边的距离,bottom是指元素下边界距窗口最上面的距离。

思路分析

通过这个 API,我们就很容易获取img元素相对于视口的顶点位置rectObject.top,只要这个值小于浏览器的高度window.innerHeight就说明进入可视区域:

function isInSight(el){
  const bound = el.getBoundingClientRect();
  const clientHeight = window.innerHeight;
  return bound.top <= clientHeight;
}

代码实现

这里结合第一种实现方式,做下改造,就得到了:

function loadImg(el){
 if(!el.src){
   const source = el.getAttribute('data-src');;
   el.src = source;
 }
}
function checkImgs(){
  const imgs = document.querySelectorAll('img');
  Array.from(imgs).forEach(el =>{
    if (isInSight(el)){
      loadImg(el);
    }
  })
}
window.onload = function(){
  checkImgs();
}
document.onscroll = function () {
  checkImgs();
}

基于 IntersectionObserver 实现图片懒加载

概念

同样,还是先来看一下概念。

这里我们参考阮一峰大佬关于IntersectionObserver API的介绍。

我们在平时的开发中,常常需要了解某个元素是否进入了"视口"(viewport),即用户能不能看到它。

上图的绿色方块不断滚动,顶部会提示它的可见性。

传统的实现方法是,监听到scroll事件后,调用目标元素(绿色方块)的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件密集发生,计算量很大,容易造成性能问题。

目前有一个新的 IntersectionObserver API,可以自动"观察"元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做交叉观察器

使用

它的用法也非常简单。

var io = new IntersectionObserver(callback, option);

上面代码中,IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数,option是配置对象(该参数可选)。

构造函数的返回值是一个观察器实例。实例的observe方法可以指定观察哪个 DOM 节点。

// 开始观察
io.observe(document.getElementById('container'));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();

上面代码中,observe的参数是一个 DOM 节点对象。

如果要观察多个节点,就要多次调用这个方法。

io.observe(elementA);
io.observe(elementB);

代码实现

看完相关的API,下面就让我们基于IntersectionObserver来实现图片懒加载:

const imgs = document.querySelectorAll('img') //获取所有待观察的目标元素
var options = {}
function lazyLoad(target) {
  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entrie => {
      if (entrie.isIntersecting) {
        const img = entrie.target;
        const src = img.getAttribute('data-src');
        img.setAttribute('src', src)
        observer.unobserve(img); // 停止监听已开始加载的图片
      }

    })
  }, options);
  observer.observe(target)
}

imgs.forEach(lazyLoad)

img.loading=lazy

最后这种相对就简单很多了,它是 Chrome 自带的原生 lazyload 属性。我们先来看下他在各大浏览器的支持程度:

其实支持程度还不是特别好,我们你的应用对于浏览器兼容性要求比较高的话,建议还是先观望一波~

它的使用也非常简单,如标题所示:

<img src="example.jpg" loading="lazy" alt="zhangxinxu" width="250" height="150">

关于原生懒加载 loading=”lazy”的更多介绍可以参考张鑫旭大佬的浏览器 IMG 图片原生懒加载 loading=”lazy”实践指南