前端優化 - 圖片懶載入

什麼情境下我們會須要圖片懶載入?

很多圖片的時候<-偏幹話。通常在網頁呈現瀑布流-依賴持續捲動頁面加載資源的時候我們會用到懶載入,常見的google圖片搜尋、facebook、twitter都有採用這個簡易的優化技巧。

它是一種設計模式,一方面因為大部分client也不一定會把整個網頁一路往下滑完才離開頁面,那我們貼心的幫他把可能非必要的資源都在一開始就加載是件浪費後端流量的無謂行為;另一方面如果不設置個條件延遲加載圖片等資源,如果資源過大或過多的情形,網頁會因為一次性加載圖片直接卡爆,這會嚴重影響使用者體驗。

實作方法

讓我們透過範例了解怎麼實現這個技巧,接下來依下面的圖片元素結構來解決這個問題。

<img 
class="lazy"
src="placeholder-image.jpg"
data-src="lazy-load-001-2x.jpg"
data-srcset="lazy-load-001-2x.jpg 2x, lazy-load-001-1x.jpg 1x"
alt="圖片失效了。"
/>
  • class - 自行定義想被歸類在要懶載入的標籤類別
  • src - 原始低耗低解析度的過渡圖片路徑或乾脆整個拿掉
  • data-src - 欲加入懶載入圖片路徑
  • data-srcset - 依各pi所對應的欲加入懶載入圖片路徑
  • alt - 圖片失效會出現的文字

Intersection Observer(主流現代瀏覽器支持)

Intersection observer API提供非同步監聽元素和其祖先或視窗交叉狀態的手段。
intersection observer
看看Web Fundamentals的範例是如何做的。

document.addEventListener('DOMContentLoaded', () => {
let lazyImages = [].slice.call(document.querySelectorAll('img.lazy'))
if("IntersectionObserver" in window) {
// 構造參數
let config = {
root: null,
rootMargin: '50px 0px',
threshold: 0.1
}

// 回呼函數
let cb = (entries, observer) => {
entries.forEach(entry => {
if(entry.isIntersecting) {
let lazyImage = entry.target
lazyImage.src = lazyImage.dataset.src
lazyImage.srcset = lazyImage.dataset.srcset
lazyImage.classList.remove("lazy")
lazyImageObserver.unobserve(lazyImage) // 停止觀察無須再觀察之元素
}
})
})

let lazyImageObserver = new IntersectionObserver(cb, config)

lazyImages.forEach(lazyImage => { // 開始觀察各目標元素
lazyImageObserver.observe(lazyImage)
})

}else {
// 不支援IntersectionObserver之其餘解法
}

}

構造參數的屬性簡介

  • root - 根元素,若不給定值的話預設值為null-代表window可視區域的範圍,亦可以依特定需求設置成欲觀察的子元素們共同的祖先元素。
  • rootMargin - 根元素的observer之延伸外邊距,設定方式跟CSS的外邊距一樣,我們通常會希望圖片在某個緩衝區就開始準備加載圖片而不是直到實際進入viewport才開始執行任務,那樣太慢了且會使client體驗很差。透過設定root的下邊距能使被觀測元素及早開始交叉達到執行callback的條件。
  • threshold - 被觀測元素跟觀測元素相交(疊合)時,當intersectionRatio(兩者相交面積/被觀測元素總面積)超過threshold設定的百分比時,會觸發callback函數。
    interSectionRatio示意圖

當我們不再需要observer時(e.g.確定沒有任何需要觀察之元素時)可以移除整個事件監聽器。

imgObserver.disconnect() // 移除observer之事件監聽器

優缺點

優點

  1. 語法簡潔不複雜,開發者只需要專注在設定根元素和子元素為何、何種情況算是交叉或被觀測到、callback做什麼DOM操作即可。
  2. 相較於resize、scroll等發生事件執行回呼頻率低,對效能改善較好。

缺點

  1. 無法完全兼容各瀏覽器 - 參考Can I use,雖然大部分現代瀏覽器如Chrome、Firefox、Safari均支援,仍有少數瀏覽器無法支援該API。

事件驅動(考慮兼容性的作法)

基於前面所說的仍有少數瀏覽器不支援Intersection Observer,
另一種比較傳統的做法就是以resize、scroll、orientationChange事件觸發來檢測DOM元素是否進入視圖來進行懶載入。
看看Web Fundamentals的範例是如何做的。

document.addEventListener("DOMContentLoaded", () => {
let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"))
let active = false

const lazyLoad = () => {
if (active === false) {
active = true

setTimeout(() => {
lazyImages.forEach((lazyImage) => {
if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) &&
getComputedStyle(lazyImage).display !== "none") {
lazyImage.src = lazyImage.dataset.src
lazyImage.srcset = lazyImage.dataset.srcset
lazyImage.classList.remove("lazy")

lazyImages = lazyImages.filter(image =>image !== lazyImage)

if (lazyImages.length === 0) {
document.removeEventListener("scroll", lazyLoad)
window.removeEventListener("resize", lazyLoad)
window.removeEventListener("orientationchange", lazyLoad)
}
}
})

active = false
}, 200)
}
}

document.addEventListener("scroll", lazyLoad)
window.addEventListener("resize", lazyLoad)
window.addEventListener("orientationchange", lazyLoad)
})

優缺點

優點

  1. 相較前面的Intersection Observer,更廣泛兼容於絕大多數的瀏覽器。

缺點

  1. 效能差,事件驅動執行callBack的次數遠多於Intersection Observer。

套件

了解怎麼用基本的JS完成實作後,要是覺得自己閉門造車很麻煩或很難維護,
可以參考開源的程式碼如何實作並自行衡量是否要採用。

結論

先判斷瀏覽器是否支援Intersection Observer,支援就優先使用,不支援才用事件驅動解決。
當然最好是需求方開的全都是支援的瀏覽器啦,省下功夫做別的事。

參考