如何用 Intersection Observer API 實作 Infinite Scroll/Lazy Loading

December 21, 2019

分類標籤:javascript web browser

現代瀏覽器提供的 Intersection Observer API 非常適合用來實現 Infinite Scroll 和 Lazy Loading 等前端常見的需求。這篇文章將會教你 Intersection Observer API 的用法,不僅程式碼更簡潔,而且效能比起監聽 scroll event 更好,此外瀏覽器支援度也不錯,就讓我們一起來看看吧!

目錄

Intersection Observer API

前端經常會需要偵測某個元素是否出現在可視範圍內,像是:

  • Lazy loading 圖片:當圖片出現在可視範圍內才開始下載
  • 無限捲軸 (Infinite Scroll):當捲動到頁面的底端時,載入更多內容
  • 當元素出現在可視範圍內才顯示動畫
  • 計算廣告在頁面上曝光的次數

過往的實作方式是監聽 scroll 事件,並利用 getBoundingClientRect() 計算元素和可視範圍的相對位置。

這樣的做法會有的潛在問題是,必須在 main thread 不斷做計算。雖然我們可以很小心地控制每次的處理不要花太多時間,但是我們的網頁有可能同時使用各種第三方套件實現無限捲軸、上百張圖片的 lazy loading、還有廣告成效計算,其中都採用 event handler + main thread 做計算的實現方法,累加起來很容易超過每個 frame 應有的執行時間限制 16ms (1000ms/60 frames ~ 16ms/frame),造成畫面更新率低於60fps、出現明顯的卡頓。

Intersection Observer API 可以幫你解決這些問題。

Intersection Observer API 的核心精神是「當重疊到某個百分比時,呼叫我的 callback function 做某件事」。他會幫你在背景監控元素的重疊程度,只在你設定的條件發生時呼叫你所提供的 callback,再也不需要時時刻刻佔用main thread去做監控的工作!

注意 Intersection Observer API 並沒有辦法精準確地剛好在重疊了幾個pixel的時候通知你,但是可以涵蓋大多數不需要 100% 準確的使用情境。

Root 和 Target

在 Intersection Observer API 中有兩個角色:root 和 target。

Root 指的是外層的容器,target 是容器內會出現的元素,root 必須是 target 的 ancestor 元素。

隨著捲軸捲動的程度或樣式的不同,target 的可見程度,可以用一個介於 0 ~ 1 的浮點數來表示。

例如,當捲軸捲到 target 剛好出現在 root 可視範圍或是完整離開可視範圍的那一瞬間,可見程度是 0;當 target 完整的出現在 root 的可視範圍內,可見程度是 1。如果 target 的高度是 root 可視範圍的兩倍,那最高的可見程度只會有 0.5。

以下提供一個簡單的demo,可以觀察看看捲軸捲動時,重疊程度數字的變化:

See the Pen IntersectionObserver Demo by Shubo Chao (@shubochao) on CodePen.

創造 Intersection Observer

首先我們需要創造一個 observer 物件並指定 root:

const options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 0.5,
};

const observer = new IntersectionObserver(callback, options);

IntersectionObserver(callback, options) 接受兩個參數:

  • callback: 當指定的重疊條件發生時要執行的 callback 函式。
  • options
    • root:欲觀察的 root 元素,不特別指定或是 null 時等於瀏覽器的可視範圍 (browser viewport)。
    • rootMargin:用來改變 root 元素觀察的範圍。
    • threshold:target的可見程度。可以給浮點數或是給浮點數的 array,例如 [0, 0.25, 0.5, 0.75, 1],每當 target 的可見程度高於 threshold 時,callback 都會被觸發。

觀察 target

接下來我們要用 observer.observe() 指定欲觀察的 target:

const target = document.querySelector('#listItem');
observer.observe(target);

當 target 的可視範圍超過指定的 thresholdcallback 就會被呼叫。

同一個 observer 可以同時觀察很多個 target。

callback 函式

const callback = (entries, observer) => {
  entries.forEach(entry => {
    // Do something...
  });
}

callback 函式接受兩個參數:

  • entriesIntersectionObserverEntry 的 array,包含所有可見程度超過 threshold 的 target 的資訊。其中幾個常用屬性:
    • isIntersecting:target 是否可見,即使只有 1px 也算
    • intersectionRatio:target 可見比例 (相對於自身完整高度)
    • 其他請參閱 IntersectionObserverEntry
  • observerobserver 本身。

完整用法範例

const root = document.querySelector('#root');

const options = {
  root,
  threshold: [0, 0.2, 0.4, 0.6, 0.8, 1],
};

const callback = (entries, observer) => {
    entries.forEach(entry => {
      // Do something...
    });
};

const observer = new IntersectionObserver(callback, options);

const target = document.querySelector('#target');
observer.observe(target);

應用

無限捲軸

在文章列表的最後面放一個 sentinel 元素,進入可視範圍表示已至列表最末端,可以載入新的文章:

See the Pen Infinite Scroll with Intersection Observer API by Shubo Chao (@shubochao) on CodePen.

廣告在可視範圍內出現了多久?

Timing element visibility with the Intersection Observer API - MDN

MDN上的範例,有點長就不細說了~

支援度

Can I Use IntersectionObserver?

約9成的瀏覽器支援度,如果對跨瀏覽器要求不會太嚴格的話可以使用。或是引入 Polyfill

Reference


Profile picture

Shubo Chao 軟體工程師,目前大多專注於前端開發