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

lazyloading

首先拿出一個網站,然後在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

1
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 Intersection_Observer_API

這方法有非常大的問題,就是不支援全部瀏覽器,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。

1
<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

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

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

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

  • Source code
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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灰色區塊。

1
2
3
4
5
6
7
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就是這樣處理的。

1
2
3
4
5
6
7
8
9
10
11
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://iandays.com/images/rabbit.png', setRatio);

seo 額外處理

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

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

心得

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

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

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

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