Day02 深入了解 Lazy-load 的背後實作 - Intersection Observer API


Day02  深入了解 Lazy-load 的背後實作 - Intersection Observer API

簡介 LazyLazy-loading-loading

使用 Lighthouse 檢測你開發的網頁,常會看到「Offscreen images」這個檢測指標,指的是載入網頁時花了多少額外時間載了第一屏幕畫面以外、可能不是當前最急需的檔案,這是優化 Page load time 的重點項目之一。

而 lazy-loading 技術正式想解決這個問題,如果能讓使用者資源集中花在第一屏畫面的載入,讓其他檔案都在後續需要時才載入,如下方範例中的圖片都是延遲載入的。

使用現有套件實作 lazy-loading

在網路上有許多可以達成 lazy-loading 的套件,比如這一個在 github 超過 8k 星星的知名套件( tuupola 的 lazyload),它是以 Vanilla JavaScript 實作的並只針對 image 類型做 lazy-loading。

使用方式分為以下三個部分

  1. 載入套件

    https://cdn.jsdelivr.net/npm/lazyload@2.0.0-rc.2/lazyload.js
    
  2. 將 Offscreen image 載入連結替換成 data-src 標籤,並加上 class 作為之後可以指定的 selector 目標。

    一般圖片

    <img src="<image-url>" width=400 height=400>
    

    lazy-loading 圖片

    <img class="lazyload" data-src="<image-url>" width=400 height=400>
    
  3. 在頁面載入後,針對這些 lazyload執行 lazyload 套件的

    lazyload();
    

解析 lazyload 功能背後原始碼

當執行

lazyload();

會執行以下程式

// default settings
const defaults = {
    src: "data-src",
    srcset: "data-srcset",
    selector: ".lazyload",
    root: null,
    rootMargin: "0px",
    threshold: 0
};
this.images = document.querySelectorAll(this.settings.selector);
---

let observerConfig = {
    root: this.settings.root,
    rootMargin: this.settings.rootMargin,
    threshold: [this.settings.threshold]
};

this.observer = new IntersectionObserver(function(entries) {
    Array.prototype.forEach.call(entries, function (entry) {
        if (entry.isIntersecting) {
            self.observer.unobserve(entry.target);
            let src = entry.target.getAttribute(self.settings.src);
            let srcset = entry.target.getAttribute(self.settings.srcset);
            if ("img" === entry.target.tagName.toLowerCase()) {
                if (src) {
                    entry.target.src = src;
                }
                if (srcset) {
                    entry.target.srcset = srcset;
                }
            } else {
                entry.target.style.backgroundImage = "url(" + src + ")";
            }
        }
    });
}, observerConfig);

Array.prototype.forEach.call(this.images, function (image) {
    self.observer.observe(image);
});

仔細看就會發現 IntersectionObserver這個上篇文章就有提到的技術,而此套件主要就是要把標有 lazyload 類別名稱的 lazy-loading image 們都加入 observe 的對象中,當這些元件進入畫面時就會自動觸發載入圖片的動作。

// selector =>  ".lazyload"
this.images = document.querySelectorAll(this.settings.selector);
Array.prototype.forEach.call(this.images, function (image) {
    self.observer.observe(image);
});

IntersectionObserver 第一個參數是 callback,指的就是載入圖片的,我們可以主要看到當需要載入圖片是去找得 data-src 上撰寫的圖片連結,這原本是不會被 HTML 解析的連結,將此改填入到 img 標籤的 src 上,瀏覽器立馬會去執行載入圖片的動作,是不是意外覺得簡單呢。

// src =>  "data-src"
// srcset => "data-srcset"

function(entries) {
    Array.prototype.forEach.call(entries, function (entry) {
        if (entry.isIntersecting) {
            self.observer.unobserve(entry.target);
            // 取得 data-src 之前藏放的圖片連結資料
            let src = entry.target.getAttribute(self.settings.src);
            let srcset = entry.target.getAttribute(self.settings.srcset);
            if ("img" === entry.target.tagName.toLowerCase()) {
                if (src) {
                    // 改放入到 img src 終讓頁面可以讀取 
                    entry.target.src = src;
                }
                if (srcset) {
                    entry.target.srcset = srcset;
                }
            } else {
                entry.target.style.backgroundImage = "url(" + src + ")";
            }
        }
    }
}

IntersectionObserver 第二個參數是 option(config),指的就是指定觸發的時機,引用上篇文章寫到的解說,就可以知道這個觸發時機是當 obsered 元件一出現在當前裝置畫面時就觸發 callback。

  • root 指的是監聽的區塊,填入 null 則使用預設的裝置 viewpoint,即是視覺上整個畫面
  • rootMargin 可以用來刪減監聽的區塊,使用就如同 css margin 比如想以 Nav 邊界為觸發點可以將第一個參數設定為負的 Nav Height
  • threshold 可以填入 0 ~ 1 的浮點數,在監聽元件出現比例佔達到時則觸發事件,這邊設定 0 就代表在元件「剛出現」及「剛消失」時觸發
// root => null
// rootMargin => 0px
// threshold =>  0

let observerConfig = {
    root: this.settings.root,
    rootMargin: this.settings.rootMargin,
    threshold: [this.settings.threshold]
};

看完,試著自己簡單實作一次吧

const selector = ".lazyload";
const dataSrc = "data-src";
const observerConfig = {
    root: null,
    rootMargin: '0px',
    threshold: [0]
};

const callback = function(entries, selfObserver) {
    Array.prototype.forEach.call(entries, function (entry) {
        if (entry.isIntersecting) {
            selfObserver.unobserve(entry.target);
            let src = entry.target.getAttribute(dataSrc);
            if ("img" === entry.target.tagName.toLowerCase()) {
                if (src) {
                    entry.target.src = src;
                }
            }
        }
    });
}

let $images = document.querySelectorAll(selector);
let observer = new IntersectionObserver(callback, observerConfig);

Array.prototype.forEach.call($images, function (image) {
    observer.observe(image);
});

小結

今天針對 Lazy-loading 以及實作背後使用的 Intersection Observer 做了介紹,希望大家會喜歡這類一起看原始碼類型的文章。

此外,想記錄一下其中有遺漏了一些細節,如原始碼中有寫到 srcset 這個是關於 響應式圖片載入 的實作,如果對這議題有興趣歡迎看小弟之前寫的文章

研究過程中也有看到一些 lazy-loading 相關議題,如果有興趣或有實際遇到可以在下留言一起來討論跟研究,謝謝大家。

  • 尚未載入圖片時如何設定精確 width、height,減少仔入圖片後的畫面變化過大
  • 如何設定 loading image ,甚至像 Medium 網站可以先載入一個解析度極低的圖片作為初始圖片
  • 搜尋引擎是否可以正確抓取 lazy-loading image,SEO 如何實作

參考資料

#Web #Web API #IntersectionObserver #javascript #MDN #LazyLoad
近期正式轉職成為一個前端工程師 在工作的 Code Base 看多許多從未使用過的 Web API 想透過此次黑客松來翻翻文件 並把感覺實用、有趣的學習並紀錄起來 ~






Related Posts

[day 01] this & bind: 你不能不知道的

[day 01] this & bind: 你不能不知道的

MTR04_0821

MTR04_0821

《寫給所有人的運算思維入門》- 寫程式到底是在幹嘛?

《寫給所有人的運算思維入門》- 寫程式到底是在幹嘛?



Comments