IntersectionObserver API を使用して layLoading を実装した。
lazyLoading(遅延読込)の必要性
ユーザーに必要のないデータを読み込むことは、多くの無駄(データ通信量、処理時間、バッテリー、その他システムのリソース)を産む。特にイメージ、動画はなるべく遅延読込をするべきである。
IntersectionObserver
これです。以下がわかりやすい。
MDN web docs
- root となる DOMElement と target となる DOMElement を指定し、それぞれがの要素が重なった場合に callback を実行する API。
- 監視には observe メソッドを使用する。
- このメソッドには引数として、以下の二つを渡す
- IntersectionObserverInit…指定するオプション(root と target の交差率など)
- IntersectionObserverCallback…検出時に発火する callback Function
まだDraftなのでPolyfillが必要
パフォーマンスが悪い。
潜在的なパフォーマンスの問題があります。繰り返しの 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が使用されている。