图片预加载和懒加载以及优先级 | 青训营笔记

830 阅读9分钟

图片预加载与懒加载

这是我参与「第四届青训营」笔记创作活动的第5天

这篇文章来谈谈前端的预加载与懒加载, 因为自己的项目中最接近的是图片预加载, 这里就以预加载为出发点深入了解了下, 本篇文章的组织形式如下

在图片的加载策略之前,我们先来了解下html网页中,不同位置的图片分别是在什么时候发起图片资源请求的

发起图片请求的时机

img 标签

img 标签会在 html 渲染解析到 img src 值, 则浏览器会立刻开启一个线程去请求该资源. 正常情况是解析到了 src 便发起请求

  1. img 标签隐藏 通过 css 样式隐藏 img 的显示
<img src="img.jpg" style="display:none"/>
<img src="img.jpg" style="display:none"/>

浏览器只会发出一次请求, 因为在发出请求前, 会检验是否有缓存, 有缓存就从缓存读取.

img 在background中

  1. 重复背景, 只发起一次请求
<style type="text/css">   
    .test1 { background: url(bg1.jpg) }   
    .test2 { background: url(bg1.jpg) }   
</style>   
<div class="test1">test1</div>   
<div class="test2">test2</div>  
  1. 隐藏元素背景.

Opera 和Firefox对display:none的元素的背景,不会立即发生请求,只有当其display 不为none才会发起图片请求。其他浏览器则是立即发起请求

<style type="text/css">   
    .test3 { display: none; background: url(bg2.jpg) }   
</style>   
<div class="test3">test1</div>  
  1. 重写背景
<style type="text/css">   
    .test1 { background: url(bg1.jpg) }   
    .test1 { background: url(bg2.jpg) }   
</style>   
<div class="test1">test1</div>  

重写背景,浏览器只会请求覆盖的那个背景图

  1. 多重背景 对全部的背景都会请求
<style type="text/css">   
    .test1 { background-image:url("haorooms.jpg"),url("http2.jpg"); }   
</style>   
<div class="test1">test1</div>
  1. 元素不存在,但是设置了背景
<style type="text/css">   
    .test3 { background: url(bg3.jpg) }   
    .test4 { background: url(bg4.jpg) }   
</style>   
<div class="test3">test1</div>   

.test4 并不存在,这个时候,浏览器并不会去请求bg3.jpg,当且仅当背景的应用元素存在时(不管在当前是显示还是不显示),才会发生请求

  1. hover背景
<style type="text/css">   
    a.test1 { background: url(haorooms.jpg); }   
    a.test1:hover { background: url(http2.jpg); }   
</style>   
<div href="#" class="test1">test1</div>

触发hover的时候,才会请求hover下的背景。在实际中,会遇到这个背景图初次显示闪一下的情况,如果要优化,就预加载这张图即可。

  1. js动态生成img并赋值
<script type="text/javascript">   
    var el = document.createElement('div');   
    el.innerHTML = '<img src="haorooms.jpg" />';   
    //document.body.appendChild(el);   
</script> 

只有Opera 不会马上请求图片,其他浏览器都是执行了代码就发起请求,Opera一定要元素添加到dom时,才会发出请求

图片预加载

资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。预加载简单来说就是将所有所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。

实现方法主要有以下3个: 方法一:用CSS和JavaScript实现预加载 方法二:仅使用JavaScript实现预加载 方法三:使用Ajax实现预加载

方法一: 用CSS和JS实现预加载

单纯使用 CSS , 可容易, 高校地预加载图片, 代码如下

#preload-01 { background: url(http://domain.tld/image-01.png) no-repeat -9999px -9999px; }  

#preload-02 { background: url(http://domain.tld/image-02.png) no-repeat -9999px -9999px; }  

#preload-03 { background: url(http://domain.tld/image-03.png) no-repeat -9999px -9999px; }

将这三个ID选择器应用到(X)html元素中,我们便可通过 CSSbackground 属性将图片预加载到屏幕外的背景上。

问题: 该方法虽然高效,但仍有改进余地。使用该法加载的图片会同页面的其他内容一起加载,增加了页面的整体加载时间。

为了解决这个问题,我们增加了一些JavaScript代码,来推迟预加载的时间,直到页面加载完毕。代码如下:

function preloader() {
    if (document.getElementById) {
        document.getElementById("preload-01").style.background = "url(http://domain.tld/image-01.png) no-repeat -9999px -9999px";
        document.getElementById("preload-02").style.background = "url(http://domain.tld/image-02.png) no-repeat -9999px -9999px";
        document.getElementById("preload-03").style.background = "url(http://domain.tld/image-03.png) no-repeat -9999px -9999px";
    }
}  

// 在 onload 之后再执行
function addLoadEvent(func) {
    var oldonload = window.onload;
    if (typeof window.onload != 'function') {
        window.onload = func;
    } else {
        window.onload = function() {
            if (oldonload) {
                oldonload();
            }
            func();
        }  
    }
}
addLoadEvent(preloader);

该脚本的第一部分,我们获取使用类选择器的元素,并为其设置了background属性,以预加载不同的图片。

该脚本的第二部分,我们使用addLoadEvent()函数来延迟preloader()函数的加载时间,直到页面加载完毕。

如果 JavaScript 无法在用户的浏览器中正常运行,会发生什么?很简单,图片不会被预加载,当页面调用图片时,正常显示即可。

方法二: 仅用 JS 实现预加载

function preloader() {
    if (document.images) {
        var img1 = new Image();
        var img2 = new Image();
        var img3 = new Image();
        img1.src = "http://domain.tld/path/to/image-001.gif";
        img2.src = "http://domain.tld/path/to/image-002.gif";
        img3.src = "http://domain.tld/path/to/image-003.gif";
    }
}  

function addLoadEvent(func) {
    var oldonload = window.onload;
    if (typeof window.onload != 'function') {
        window.onload = func;
    } else {
        window.onload = function() {
            if (oldonload) {
                oldonload();
            }
            func();
        }
    }
}
addLoadEvent(preloader);

方法三: 使用Ajax实现预加载

该方法利用DOM,不仅仅预加载图片,还会预加载CSS、JavaScript等相关的东西。使用Ajax,比直接使用JavaScript,优越之处在于JavaScript和CSS的加载不会影响到当前页面。该方法简洁、高效。

window.onload = function() {
    setTimeout(function() {
        // XHR to request a js and a CSS
        var xhr = new XMLHttpRequest();
        xhr.open('GET', 'http://domain.tld/preload.js');
        xhr.send('');
        xhr = new XMLHttpRequest();
        xhr.open('GET', 'http://domain.tld/preload.css');
        xhr.send('');
        // preload image
        new Image().src = "http://domain.tld/preload.png";
    }, 1000);
};

浏览器加载资源的优先级

浏览器解析资源的优先级

当浏览器开始解析网页,并开始下载 图片Script 以及 CSS 等资源的时候,浏览器会为每个资源分配一个代表资源下载优先级的 fetch priority 标志。

而资源下载的顺序就取决于这个优先级标志,这个优先级标志的计算逻辑会受很多因素的影响:

  • ScriptCSSFontImage 等不同的资源类型会有不同的优先级。

  • HTML文档 中引用资源的位置或顺序也会影响资源的优先级(例如在 viewport 中的图片资源可能具有高优先级,而在 <link> 标签中加载的,阻塞渲染的 CSS 则拥有更高的优先级)。

  • preload 属性的资源有助于浏览器更快地发现资源、其实也是影响资源加载的优先级。

  • Scriptasyncdefer 属性都会影响它的优先级。

下图是大多数资源在 Chrome 中的优先级和排序方式:

img.png

可见, 不在视图区域内的图片已经就是 Low级了, 如果想要更低, 可以通过 importancce 属性来更细力度的控制资源加载的优先级,包括 linkimgscriptiframe 这些标签。

importance 属性可以指定三个值:

  • high:你认为该资源具有高优先级,并希望浏览器对其进行优先级排序。
  • low:你认为该资源的优先级较低,并希望浏览器降低其优先级。
  • auto:采用浏览器的默认优先级。

图片懒加载

这一篇主要介绍的是 getBoundingClientRectIntersectionObserver 这两个API

原理其实非常简单,主要就是需要判断元素是否进入了可视区,进入了可视区就去请求对应的图片,否则就显示一张兜底的占位图。

getBoundingClientRect

返回值是一个DOMRect对象,这个对象是由该元素的getClientRects()方法返回的一组矩形的集合,就是该元素的 CSS 边框大小。返回的结果是包含完整元素的最小矩形,并且拥有left, top, right, bottom, x, y, width, 和 height这几个以像素为单位的只读属性用于描述整个边框。除了width 和 height 以外的属性是相对于视图窗口的左上角 来计算的。

  • top、left和css中的理解很相似,width、height是元素自身的宽高;
  • right,bottom与css中的理解不一样。right是指元素右边界距窗口最左边的距离,bottom是指元素下边界距窗口最上面的距离。

这里需要给每张图片加上默认高度,不然会第一次就直接下载所有文件的!!!

当计算边界矩形时,会考虑视口区域(或其他可滚动元素)内的滚动操作,也就是说,当滚动位置发生了改变,top和left属性值就会随之立即发生变化(因此,它们的值是相对于视口的,而不是绝对的)。如果你需要获得相对于整个网页左上角定位的属性值,那么只要给top、left属性值加上当前的滚动位置(通过window.scrollX和window.scrollY),这样就可以获取与当前的滚动位置无关的值。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .img_box {
            display: block;
            width: 100%;
            height: 530px;
            margin: 0 auto 20px;
            border: 1px solid #ccc;
            /* object-fit: cover; */
        }
    </style>
</head>
<body>
<div>图片懒加载</div>
<div class="img_list">
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg1.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg2.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg3.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg4.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg5.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg6.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg1.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg2.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg3.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg4.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg5.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg6.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg1.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg2.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg3.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg4.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg5.webp"/>
    <img class="img_box" src="./bg13.webp" data-src="https://wjygrit.oss-cn-beijing.aliyuncs.com/2021/bg6.webp"/>
</div>
<script>
    function lazyLoad() {
        // 第一种方法判断图片是否进入了可视区
        // 先获取当前可视区的高度
        let viewHeight = document.documentElement.clientHeight
        console.log(viewHeight)
        // 获取当前页面所有图片
        let imgs = document.querySelectorAll('.img_box')
        imgs.forEach(item => {
            if (!item.getAttribute('src')) return
            let rect = item.getBoundingClientRect()
            console.log(rect)
            //判断元素是否在可视区
            if (rect.bottom >= 0 && rect.top < viewHeight && item.dataset.src) {
                // 当前元素距离底部距离大于等于0并且距离顶部距离小于当前可视区高度,则说明元素在可视区
                item.src = item.dataset.src
            }
        })
    }

    lazyLoad()
    window.addEventListener('scroll', lazyLoad)
</script>
</body>
</html>

IntersectionObserver

IntersectionObserver() 构造器创建并返回一个IntersectionObserver对象。 如果指定rootMargin则会检查其是否符合语法规定,检查阈值以确保全部在0.0到1.0之间,并且阈值列表会按升序排列。如果阈值列表为空,则默认为一个[0.0]的数组。

它可以异步监听目标元素与其祖先或视窗的交叉状态,注意这个接口是异步的,它不随着目标元素的滚动同步触发,所以它并不会影响页面的滚动性能。

let observer = new IntersectionObserver(callback[, options])

  • callback:当元素可见比例超过指定阈值,会调用这个回调函数,该函数接受两个参数

    • entries:一个IntersectionObserverEntry对象的数组,每个被触发的阈值,都或多或少与指定阈值有偏差。
    • observer:被调用的IntersectionObserver实例
  • options(可选)一个可以用来配置observer实例的对象。如果options未指定,observer实例默认使用文档视口作为root,并且没有margin,阈值为0%(意味着即使一像素的改变都会触发回调函数)

    • root:监听元素的祖先元素,其边界盒将被视作视口。目标在根的可见区域的的任何不可见部分都会被视为不可见。
    • rootMargin:一个在计算交叉值时添加至根的边界盒中的一组偏移量,类型为字符串(string) ,可以有效的缩小或扩大根的判定范围从而满足计算需要。
    • threshold:规定了一个监听目标与边界盒交叉区域的比例值,可以是一个具体的数值或是一组0.0到1.0之间的数组。若指定值为0.0,则意味着监听元素即使与根有1像素交叉,此元素也会被视为可见. 若指定值为1.0,则意味着整个元素都在可见范围内时才算可见。
    function lazyLoadWithobserver() {
        let observer = new IntersectionObserver((entries, observer) => {
            entries.forEach(item => {
                // 获取当前正在观察的元素
                let target = item.target
                if (item.isIntersecting && target.dataset.src) {
                    target.src = target.dataset.src
                    // 删除data-src属性
                    target.removeAttribute('data-src')
                    // 取消观察
                    observe.unobserve(item.target)
                }
            })
        })


        let imgs = document.querySelectorAll('.img_box')
        imgs.forEach(item=>{
            // 遍历观察元素
            observer.observe(item)
        })
    }

    lazyLoadWithobserver()

总结

这次详细研究了前端的 图片预加载和懒加载的原理以及实现方式, 同时也了解到了 浏览器资源优先级 这个比较偏但有用的概念, 总额来说, 关于图片预加载这里还有其他一些知识点, 如响应式图片, 但我的项目中不需要, 就不先研究了.

此篇文章已同步到我的个人博客中啦~

参考资料