圖片懶載入踩坑

hentaicracker發表於2019-03-04

原文地址:圖片懶載入踩坑

原理

對網頁載入速度影響最大的就是圖片,一張普通的圖片可能會有好幾 M 的大小,當圖片很多時,網頁的載入速度變得很緩慢。

為了優化網頁效能以及使用者體驗,我們對圖片進行懶載入

懶載入是一種對網頁效能優化的方式,它的原理是優先載入在可視區域內的圖片,而不一次性載入所以圖片。當瀏覽器滾動,圖片進入可視區時再去載入圖片。通過設定圖片的 src 屬性來讓瀏覽器發起圖片的請求。當這個屬性為空或者沒有時,就不會傳送請求。

實現

此文所涉及的懶載入皆是在垂直方向上的滾動載入,橫向滾動暫不考慮。

懶載入的實現主要是判斷當前圖片是否到了可視區域這一核心邏輯。我們先來整理一下實現思路:

  1. 拿到所有的圖片 img dom
  2. 遍歷每個圖片判斷當前圖片是否到了可視區範圍內。
  3. 如果到了就設定圖片的 src 屬性。
  4. 繫結 window 的 scroll 事件,對其進行事件監聽。

HTML 結構

<div class="container">
    <div class="img-area">
        <img id="first" data-src="./img/ceng.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/data.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/huaji.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/liqi1.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/liqi2.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/steve-jobs.jpg" alt="">
    </div>
</div>
複製程式碼

此時 img 標籤是沒有 src 屬性的,我們把真實的圖片地址放在一個屬性裡,這裡我們使用 HTML5 的 data 屬性,將真實地址放在自定義的 data-src 中。

判斷圖片是否進入了可視區域

這一邏輯有兩種方法,聽我娓娓道來。

方法一

第一種方法我們通過計算該圖片距離 document 頂部的高度是否小於當前可視區域相對於 document 頂部高度來判斷。

可視區域相對於 document 頂部高度的計算方法:

const clientHeight = document.documentElement.clientHeight; // 視口高度,也就是視窗的高度。
const scrollHeight = document.documentElement.scrollTop + clientHeight; // 滾動條偏移文件頂部的高度(也就是文件從頂部開始到可視區域被抹去的高度) + 視口高度
複製程式碼

畫了一張圖方便理解:

圖片懶載入踩坑

然後就是計算該圖片距離文件頂部的高度。有兩種方法,第一種方法是通過元素的 offsetTop 屬性來計算。從上圖我們瞭解到元素的 offsetTop 屬性是相對於一個 position非 static 的祖先元素,也就是 child.offsetParent 。同時需要將祖先元素的 border 考慮在內,我們通過child.offsetParent.clientTop可以拿到邊框厚度。

由此我們得到元素距離文件頂部的高度的計算方法:

function getTop(el, initVal) {
    let top = el.offsetTop + initVal;
    if (el.offsetParent !== null) {
        top += el.offsetParent.clientTop;
        return getTop(el.offsetParent, top);
    } else {
        return top;
    }
}
複製程式碼

這裡的這個方法使用了 尾遞迴呼叫 。可以提高遞迴效能。當然這裡也可以用迴圈來實現:

function getTop(el) {
    let top = el.offsetTop;
    var parent = el.offsetParent;
    while(parent !== null) {
        top += parent.offsetTop + parent.clientTop;
        parent = parent.offsetParent;
    }
    return top;
}
複製程式碼

第二種方法是使用 element.getBoundingClientRect() API 直接得到 top 值。

getBoundingClientRect 的返回值如下圖:

圖片懶載入踩坑

var first  = document.getElementById('first');
getTop(first, 0);  // 130
console.log(first.getBoundingClientRect().top); // 130
複製程式碼

於是我們得到判斷方法:

function inSight(el) {
    const clientHeight = document.documentElement.clientHeight;
    const scrollHeight = document.documentElement.scrollTop + clientHeight;
    // 方法一
    return getTop(el, 0) < scrollHeight;
    // 方法二
    // return el.getBoundingClientRect().top < clientHeight;
}
複製程式碼

接下來就是對每個圖片進行判斷和賦值。

function loadImg(el) {
    if (!el.src) {
        el.src = el.dataset.src;
    }
}

function checkImgs() {
    const imgs = document.getElementsByTagName('img');
    Array.from(imgs).forEach(el => {
        if (inSight(el)) {
            loadImg(el);
        }
    })
    console.log(count++);
}
複製程式碼

最後給 window 繫結 onscroll 事件以及 onload 事件:

window.addEventListener('scroll', checkImgs, false);
window.onload = checkImgs;
複製程式碼

我們知道類似 scrollresize 這樣的事件瀏覽器可能在很短的時間內觸發很多次,為了提高網頁效能,我們需要一個節流函式來控制函式的多次觸發,在一段時間內(如 500ms)只執行一次回撥。

/**
 * 持續觸發事件,每隔一段時間,只執行一次事件。
 * @param fun 要執行的函式
 * @param delay 延遲時間
 * @param time 在 time 時間內必須執行一次
 */
function throttle(fun, delay, time) {
    var timeout;
    var previous = +new Date();
    return function () {
        var now = +new Date();
        var context = this;
        var args = arguments;
        clearTimeout(timeout);
        if (now - previous >= time) {
            fun.apply(context, args);
            previous = now;
        } else {
            timeout = setTimeout(function () {
                fun.apply(context, args);
            }, delay);
        }
    }
}
window.addEventListener('scroll', throttle(checkImgs, 200, 1000), false);
複製程式碼

方法二

HTML5 有一個新的 IntersectionObserver API,它可以自動觀察元素是否可見。

主要用法:

var observer = new IntersectionObserver(callback, option);

// 開始觀察
observer.observe(document.getElementById('first'));

// 停止觀察
observer.unobserve(document.getElementById('first'));

// 關閉觀察器
observer.disconnect();
複製程式碼

目標的可見性發生變化時就會呼叫觀察器的 callback。

function callback(changes: IntersectionObserverEntry[]) {
    console.log(changes[0])
}

// IntersectionObserverEntry
{
    time: 29.499999713152647,
    intersectionRatio: 1,
    boundingClientRect: DOMRectReadOnly {
        bottom: 144,
        height: 4,
        left: 289,
        right: 293,
        top: 140,
        width: 4,
        x: 289,
        y: 140
    },
    intersectionRect: DOMRectReadOnly,
    isIntersecting: true,
    rootBounds: DOMRectReadOnly,
    target: img#first
}
複製程式碼

詳細釋義:

  • time: 可見性發生變化的時間,是一個高精度時間戳,單位為毫秒
  • intersectionRatio: 目標元素的可見比例,即 intersectionRect 佔 boundingClientRect 的比例,完全可見時為 1 ,完全不可見時小於等於 0
  • boundingClientRect: 目標元素的矩形區域的資訊
  • intersectionRect: 目標元素與視口(或根元素)的交叉區域的資訊
  • rootBounds: 根元素的矩形區域的資訊,getBoundingClientRect() 方法的返回值,如果沒有根元素(即直接相對於視口滾動),則返回 null
  • isIntersecting: 是否進入了視口,boolean 值
  • target: 被觀察的目標元素,是一個 DOM 節點物件

使用 IntersectionObserver 實現圖片懶載入:

function query(tag) {
    return Array.from(document.getElementsByTagName(tag));
}
var observer = new IntersectionObserver(
    (changes) => {
        changes.forEach((change) => {
            if (change.intersectionRatio > 0) {
                var img = change.target;
                img.src = img.dataset.src;
                observer.unobserve(img);
            }
        })
    }
)
query('img').forEach((item) => {
    observer.observe(item);
})
複製程式碼

完整程式碼見 github

完 :)

相關文章