最近身邊朋友問到 google speedtest 分數優化處理,剛好處理完網頁優化專案,雖然分數尚有進步空間…,lazyload 幾乎是網站必備的優化處理,分享一點處理 lazyload 心得。

首先拿出一個網站,然後在google speedtest輸入送出跑分,如果沒特別處理圖片的話,就會看到精美的項目出現 延後載入畫面外圖片。這代表網站沒有處理延後載入圖片。假設網站有多圖片、或高畫質圖片,那 lazyload image 是不錯的優化處理。

lazyload 範例 : unsplash

上面的 unsplash 是知名的免費圖庫網站,它就有做了 lazyload 的處理,你再網頁往下滾過程,會發現區塊先有各種顏色的背景,再開始慢慢載入圖片。

Google Guide Lazyload

google 這篇文章,有非常詳盡的教學,告訴你每種處理方法之間的差異。(推薦看完)

Google website Guide: Lazy Loading Images

實作方法 inline images

這是最常見的處理方法,直接利用 javascript 搭配 viewport 判斷,把圖片網址由 data-src 轉為加入 src 載入圖片 ,實際作法分為三種。Intersection observer、event handler、原生 chrome 支援。

  • before
    <img data-src="https://fakeimg.pl/250x100/" class="lazyload">
  • after
    <img src="https://fakeimg.pl/250x100/">

Intersection observer

IntersectionObserver 是新的瀏覽器 api,主要處理 element 與 viewport 之間處理,當目標與 element 交會則會處理互動,效能相對於傳統做法相對好。

Google article: Trust is Good, Observation is Better—Intersection Observer v2

var observer = new IntersectionObserver(callback[, options]);

上面的參數 callback 是當 viewport 與 element 交會時會觸發,options 則是可以使用 rootrootMarginthreshold

root 可以定義我們判斷這整個 viewport 的大外層,rootMargin 則是可以給予 root 大外層假想的 margin,讓我們可以讓外層 viewport 提早被觸發,進一步提早 callback,threshold 則是可以定義出 viewport 與 element 之間觸發的比例 0~1.0,預設為 0,當我設定 [0, 0.5, 1],代表他會在三個 viewport 比例 callback。

IntersectionObserver DOC:MDN IntersectionObserverAPI

這方法有非常大的問題,就是不支援全部瀏覽器,ie 全部不支援,需要引入 IntersectionObserver polyfill 處理,或是判斷 window API 有沒有 IntersectionObserver,沒有就走 event handler 方法,

第一個方法缺點就是要載資源,背後實作也是用 scroll listener,沒有說你用了 polyfill 就享有效能好的福利,第二種方法缺點就是要維運兩套 lazyload trigger function,對開發來說成本頗高。

caniuse intersectionObserver : 悲劇的 IE “悲劇的 IE”)

頁面範例: codepen demo

Event handler

這是最常見的做法,監聽瀏覽器滾動事件,利用 getBoundingClientRect().top,來判斷這個 element 是不是在使用者的視點內,如果在視點內的話,我們就將 img src 轉變成真正的 url,達到延後載入的效果。

下面的範例是 google guide 的示範,基本上實作邏輯都差不多,會用到 scroll listener,搭配 throttle 避免 scroll 過度觸發判斷 function,當所有 element 完成 lazyload 移除 Listener,還有監聽 resize 畫面拉伸、orientationchange 倒換畫面。

頁面範例: codepen demo

Chrome 原生支援

chrome version 75 才會 release 的功能,現在可以先手動開啟設定,chrome 網址列輸入chrome://flags/#enable-lazy-image-loading,右邊選項改為 enabled。

<img loading='lazy' src='https://placekitten.com/400/400' width='400' height='400' alt=''>

頁面範例: chrome lazyload demo

chromestatus: chrome lazyload feature

lazyload 套件

隨便搜尋 lazyload plugin,就會出現各種 lazyload 的套件,這邊就不贅述了。如果還是沒方向的話,我目前專案上有用到 lazySize,config 很多也是不錯用。

修飾畫面抖動問題

但你滾動會發現右邊的 scroll bar 會慢慢長出來,要解決的辦法就是必須模擬圖片高度,撐出 lazyload 的區塊。保持高度一致不抖動畫面。

  • 示範圖片
    rabbit
    rabbit

上面這張圖片的寬度是 2955、高度 1516,比例約是 29: 15,我們可以利用圖片比例來產生接近圖片實際大小的灰階區塊。

接下來使用 padding-top 來撐出高度,因為我們用的是比例,所以畫面寬度變化,區塊的高度也會隨之變化。直接用上面的 IntersectionObserver 修改,多做的動作就是 img 要先填上各自的比例,當載入畫面時填上 padding-top,產生區塊的灰色高度。

ps.codepen 的範例有加上 setTimeout 500ms,特意讓大家看到 lazyload 灰階區塊。

  • Source code
document.addEventListener('DOMContentLoaded', function() {
    var lazyImages = [].slice.call(document.querySelectorAll('img.lazyload'));

    if ('IntersectionObserver' in window) {
        let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
            entries.forEach(function(entry) {
                if (entry.isIntersecting) {
                    let lazyImage = entry.target;
                    lazyImage.src = lazyImage.dataset.src;
                    lazyImage.classList.remove('lazyload');

                    //clear padding-top
                    lazyImage.style.paddingTop = '';
                    lazyImageObserver.unobserve(lazyImage);
                }
            });
        });

        lazyImages.forEach(function(lazyImage) {
            // get image ratio
            var ratio = lazyImage.dataset.ratio;
            // create padding-top
            lazyImage.style.paddingTop = ratio + '%';
            lazyImageObserver.observe(lazyImage);
        });
    } else {
        // Possibly fall back to a more compatible method here
    }
});

頁面範例: codepen demo

另外還可以加上 onload listener 讓載入圖片過程,保持 lazyload 灰色區塊。

lazyImage.src = lazyImage.dataset.src;
lazyImage.onload = function() {
    lazyImage.classList.remove('lazyload');

    //clear padding-top
    lazyImage.style.paddingTop = '';
};

w3c ration 教學:Aspect Ratio / Height Equal to Width - W3Schools

產生圖片比例 Ratio

這在實作上必須倚賴 javascript,或後端用其他工具產生,例如說圖片上傳過程後端會另外 call api,取得圖片的比例。

這邊先只討論 javascript,利用 Image API 也可以輕鬆做到,下面的方法就可以拿到圖片的 ratio,setRatio 則是可以當作儲存圖片的方法,當使用者上傳圖片,會再經過 getImageRatio,再帶入 save data function 利用 callback,當取得圖片比例再提供 ratio 給後端儲存。

這方法需要一個前提條件,一種是圖片寬度再 container 下需要寬滿版,因為 padding-top 推出來的高度,是相對於 container 寬度乘出來的。有些人會利用 div container img 概念處理,只要變化 container 寬度,一樣可以兼容 lazyload,medium 就是這樣處理的。

function getImageRatio(url, callback) {
    var img = new Image();
    img.onload = function() {
        callback((this.height / this.width) * 100);
    };
    img.src = url;
}
function setRatio(ratio) {
    console.log(ratio);
}
getImageRatio('https://ianccy.com/images/rabbit.png', setRatio);

seo 額外處理

雖然 googlebot 可以執行 javascript,但是其他爬蟲不一定可以,所以要特別標明出 noscript 的 img html。

<img class="lazyload" data-ratio="51.24481327800829" data-src="https://ianccy.com/images/rabbit.png" alt="rabbit">
<noscript><img src="https://ianccy.com/images/rabbit.png" alt="rabbit" /></noscript>

心得

實際上處理 lazyload 是比較麻煩,要做出 lazyload 區塊灰階,就需要知道圖片比例。至於如何產生灰階,又分為存資料前預先做好 style,或載入頁面再依賴 javascript 產生。個人較偏向預先做好 style,減少使用使用者資源。

為了優化效能,目前專案部分有使用 IntersectionObserver,但要支援萬惡的 IE,要再額外載入 polyfill,這點目前還在思考最佳解,目前是判斷 window 不含 IntersectionObserver 才會執行 polyfill。

結論: lazy loading image 對網站是很合理的處理優化,使用者沒看到的區塊本來就不需要浪費網路載入。

感謝閱讀,以上有問題歡迎留言,或是傳訊息。