Henry's Blog

使用 IntersectionObserver 達到 lazy loading 效果

最近看到這篇 SEO 相關文章中有提到使用 lazy loading 以降低效能使用(不確定是否對 SEO 有幫助?),才發現沒有研究過 lazy loading 的實作,甚至沒聽過 Intersection Observer。

Lazy loading 的實作方法

在 IntersectionObserver 出現之前,常見的實作方法有以下兩種,目的都是要取得要監聽的元素當前之於 viewport 的位置:

兩種方法都可以達到目的,但缺點是會拖累網頁的效能,因為每次執行 getBoundingClientRect() 都會觸發瀏覽器 re-layout 整個頁面,這也是為什麼 IntersectionObserver 會被設計出來的原因

如何使用 IntersectionObserver

IntersectionObserver 其實就是使用 Observer 的設計模式,當需要監聽時 observe,不需要時 unobserve

先看範例程式碼

const io = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    console.log(entry);
  })
}, {
  /* 非必要選項,不填入會套用預設值 */
});

// 開始監聽元素
io.observe(element);

// 停止監聽元素
// io.unobserve(element);

// 停止 io 監聽全部元素
// io.disconnect()

當沒有帶入第二個選填參數時,io 的 callback function 會在任一被監聽元素進入/離開 viewport 時被執行,而 callback 的參數 entries 是一個由 IntersectionObserverEntry 所組成的 array,我們接下來就是要對它進行操作

如果需要監聽多個元素,像是頁面中有許多圖片待載入,建議將全部元素使用同一個 IntersectionObserver 監聽即可,例如:

const io = new IntersectionObserver(entries => {
 // 對 entries 的操作
 // ......
 // ......
});

io.observe(element1);
io.observe(element2);
io.observe(element3);
io.observe(element4);
io.observe(element5);
// ......

IntersectionObserverEntry 是什麼&有什麼用處

大家可以使用下面範例玩看看(記得打開 console 看印出的結果)印出的 entries/entry 是什麼(在本頁面或是 codepen 都可以)

See the Pen Basic IntersectionObserver by Henry (@chou07) on CodePen.

可以看到印出來的 entry 結構長這樣

🔽[IntersectionObserverEntry]
    time: 3893.92
    rootBounds: null
    isIntersecting: false
    isVisible: false
  🔽boundingClientRect: ClientRect
    // ...
  🔽intersectionRect: ClientRect
    // ...
    intersectionRatio: 0.1669006496667862
  🔽target: div#box
    // ...

這裡簡單說明每個屬性的意思:

這裡很重要的一點是 IntersectionObserver 是非同步的監聽 root element / observed element 之間的變化,而 callback 只會在瀏覽器 idle 時才會被觸發執行。如此一來可以大幅降低效能需求

不過 callback 執行是在主執行緒上(main thread),所以如果在 callback 一次執行太多事情還是會造成 blocking

IntersectionObserver's options

上面有提到當建立 IntersectionObserver instance 時,第二個參數是選填的 options,其中包含了

當中比較直得注意的是 thresholds,可以把它想成百分比,callback 會依照該比例依序被觸發,舉例來說:

rootrootMargin 平常應該都不會改動到,除非只想要監聽特定元素會是想要讓 root element 被計算的面積變大/縮小

範例

new IntersectionObserver(() => {/* ... */}, {
  root: null,
  // 如同 CSS 的 margin,rootMargin 可以是正值或是負值、可以是 px 或是 %
  rootMargin: '0px',
  thresholds: [0, 1]
});

實作 lazy loading

基於上面學的內容做了個簡單的範例模擬 lazy loading

範例連結

滾動到被監聽元素時,才會執行 fetchImage、且在兩秒後更新圖片

但其實這個範例並不完美,因為雖然捲動到該元素才會開始更新圖片,但往上捲動時同樣會觸發更新圖片(因為 thresholds: 0,所以進入或是離開 root element 都會觸發),只不過我使用同一張圖片所以看不出差異,這部分就交給你優化囉!

支援度

caniuse 可以看出其實目前 IntersectionObserver 的支援度並不是很好,尤其 IE 完全不支援,所以在使用上要注意一下。

為什麼這裡的 console 可以印出 codepen 範例的 log

最後,不知道你會不會有疑問,上面 codepen 的範例不是在 iframe 裡面嗎?為什麼在部落格的 console 也印得出結果?

這就是 IntersectionObserver 實用的地方了,即便你的網頁是被嵌在其他網頁當中的,你同樣可以知道使用者什麼時候看到你的網頁

Reference