IntersectionObserverを使ったlazyLoading

   · ☕ 3 min read

IntersectionObserver API を使用して layLoading を実装した。

lazyLoading(遅延読込)の必要性

ユーザーに必要のないデータを読み込むことは、多くの無駄(データ通信量、処理時間、バッテリー、その他システムのリソース)を産む。特にイメージ、動画はなるべく遅延読込をするべきである。1

IntersectionObserver

これです。以下がわかりやすい。
MDN web docs

  • root となる DOMElement と target となる DOMElement を指定し、それぞれがの要素が重なった場合に callback を実行する API。
  • 監視には observe メソッドを使用する。
    • このメソッドには引数として、以下の二つを渡す
      • IntersectionObserverInit…指定するオプション(root と target の交差率など)
      • IntersectionObserverCallback…検出時に発火する callback Function

まだDraftなのでPolyfillが必要2

addEventlisner(‘scroll’ callback)でよいのでは?

パフォーマンスが悪い。

潜在的なパフォーマンスの問題があります。繰り返しの setTimeout 呼び出しは(呼び出し内のコードが制限されても)無駄になる可能性があります。

イメージと動画の遅延読み込み | Web Fundamentals | Google Developers

実装

実装の目的

700 件近く存在するデータを表示する上で無駄を避けるため、無限スクロール(Twitter などで利用されている読み込み方法)のようなUIで、画面最下部到達時のみ追加データを読み込む実装をしたかった。

実装(使われる側のコード)

今回は React を使用しているため、RefObjectを使用した。下記のコードの形で、複数の RefObject を管理する Observer class を提供するようにした。

 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
30
31
32
33
34
35
import * as React from 'react'

export default class Observer {
  targetRefs: React.RefObject<HTMLDivElement>[]
  observer: IntersectionObserver

  constructor(
    options: IntersectionObserverInit,
    callback: IntersectionObserverCallback
  ) {
    this.observer = new IntersectionObserver(callback, options)
    this.targetRefs = []
  }

  public createRef = (): React.RefObject<HTMLDivElement> => {
    const targetRef = React.createRef<HTMLDivElement>()
    this.targetRefs.push(targetRef)
    return targetRef
  }

  public existRef = () =>
    this.targetRefs.length > 0 && this.targetRefs[0].current

  public start() {
    this.targetRefs.forEach((target: React.RefObject<HTMLDivElement>) => {
      this.observer.observe(target.current)
    })
  }

  public stop() {
    this.targetRefs.forEach((target: React.RefObject<HTMLDivElement>) => {
      this.observer.unobserve(target.current)
    })
  }
}

ReactHook として書いているので、useRefメソッドを使用してRefObjectを管理することも可能。

実装(使う側のコード)

 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
30
31
32
33
34
// optionsを作る
const options = {
  root: document.querySelector('#root'),
  rootMargin: '0px',
  threshold: 1.0,
}

// callbackを作る
const observeCallback: IntersectionObserverCallback = (
  entries: IntersectionObserverEntry[]
) => {
  entries.forEach((entry) => {
    const intersecting = entry.intersectionRatio !== 0
    if (intersecting && loadingObserver.existRef()) consumeStack()
  })
}

// 上記2つを使用して初期化
const loadingObserver = new Observer(options, observeCallback)

// 監視対象を作って登録。作成されたRef.Objectは別でReact.Componentに渡しておく
const ref = loadingObserver.createRef()
const Something: React.FunctionComponent = () => {
    return (<div ref={ref}>something</div>)
}

// 以下のような感じで使用する
function observeStart() {
  if (!loadingObserver.existRef()) return

  // 適当なタイミングで監視を起動/停止
  if(something) loadingObserver.start()
  if(something) loadingObserver.stop()
}

おわりに

ここで使用しています。

React Hook としてFunctionComponent で構成しているのならば、管理用class を設けるよりはCustom Hook として切り出す方が良いかもしれない。

追記

日経電子版のPWAに関する記事でも、パフォーマンス向上のためにInterSection Observer APIが使用されている。

Share on

whasse
WRITTEN BY
whasse
Web Developer