2022.04.14

コーディング

IntersectionObserverのすゝめ

「スクロールイベントってスクロールするたび何回も発火するからパフォーマンス的には良くないよね」と言えり。

ということでIntersectionObserver(インターセクションオブザーバー)をオススメしていこうと思います。

いままでスクロールイベントで発火していた様々なものをIntersectionObserverによる発火に置き換えるだけでパフォーマンスがぐっとあがること間違いなし!

IntersectionObserverとは?

IntersectionObserverは「交差観察者」の名の通り、ターゲットとなる要素が指定のビューポートと交差する変化を非同期的に監視する方法を提供するAPIです。

簡単に言うと、「この要素がここまで表示されたらなにか処理をしたい」というのを実現させてくれるAPIです。

例えば画像の遅延ロード。

今までですと遅延ロードのためのライブラリを使ったり、自分で実装する場合にはスクロールイベント内でターゲットの位置の計算をしてたりしていたと思います。

これがIntersectionObserverAPIを使用すれば、無駄なライブラリを増やすことなく、無駄なスクロールイベントを発生させることなく、簡単に実装できるのです!

さらにオプションも便利でまさに痒い所に手が届くAPIであり、

かようなAPIが欲しかりき。といふよりも、スクロールイベントが糞なり。

と諭吉も申しております。

画像遅延ロードの例

とりあえず遅延ロードを実装する際のソースコードを見てみましょう。

まずはhtml側。

<img src="" data-src="(画像のURL)" alt="" class="lazyload">

html側ではimg要素に目印となるクラスをいれます。今回は「lazyload」クラスを入れています。

  • 既にあるコードを改修する場合は「lazyload」クラスが既に使用されていないか確認してください。思ったように動かないぞ? と思っていたら、「lazyload」は遅延ロードでよく使われるクラスなので、実は既に誰かが追加した遅延ロードライブラリで使われていて動作がバッティングしていた、という報告が私から出ています。
    もしくは「lazyload」ではなく他と被らなそうなクラスにしましょう。

そして表示したい画像URLをsrc属性には入れずにdata属性に入れます。
今回はdata-src属性を用意してセットしています。

htmlでやることは以上です。

それでは、js内でIntersectionObserverを開始させましょう。

//「lazyload」クラスがついている要素を取得
const lazyImages = document.querySelectorAll(".lazyload");

// IntersectionObserverに対応しているブラウザのみ、処理を実行
if ("IntersectionObserver" in window) {

	//オプションを設定
	const options = {
		root: null,
		rootMargin: "0px",
		threshold: 0.0
	}

	//監視内容
	const lazyImageObserver = new IntersectionObserver(function(entries, observer) {
		entries.forEach(function(entry) {
			// この要素が画面に入ってきた場合
			if (entry.isIntersecting) {
				var lazyImage = entry.target;
				if (lazyImage.dataset.hasOwnProperty("src")) {
					lazyImage.src = lazyImage.dataset.src;
					delete lazyImage.dataset.src;
				}
				// ロード済みのクラス「lazyloaded」を付加
				lazyImage.classList.add("lazyloaded");
				// この画像要素を観察対象から外す
				lazyImageObserver.unobserve(lazyImage);
			}
		});
	}, options);

	//監視開始
	lazyImages.forEach(function(lazyImage) {
		lazyImageObserver.observe(lazyImage);
	});

} else {
	//IntersectionObserverに対応していないブラウザの場合の処理
	lazyImages.forEach(function(lazyImage) {
		lazyImage.src = lazyImage.dataset.src;
		delete lazyImage.dataset.src;
		lazyImage.classList.add("lazyloaded");
	})
}

2行目で監視したい要素を取得し、
8行目で交差判定に関するオプション(※後述します)を設定し、
15行目から交差した際の処理を書き、
33行目で実際に監視を開始させています。

5行目で「IntersectionObserverに対応しているブラウザかどうか」で分岐させていますが、モダンブラウザはほぼ対応しています(Can I Use?)。

IEは対応していないですが…IEは…もう、いいでしょう。2022年6月16日にサポート終了しますし、対応していない場合は遅延ロードにならないだけで画像自体は表示されるように処理を書いています。

IEユーザーのページ読み込みスピードまで考慮する必要あらず。

諭吉もこう申しております。

18行目から監視対象の要素が画面内に入ってきた際の処理を書いている訳ですが、ここでやっていることはdata-src属性に入っている画像URLをsrc属性にセットし、data-src属性は削除した後、「ロードしたよ」という意味のクラス「lazyloaded」を追加する。というものです。

こうして監視対象の要素が画面に入ってきた瞬間にsrcがセットされ、画像ロードが始まる。という訳ですね。

👇コードペンで動かしてみましょう。

img要素に
opacity: 0;
を当てておいて、lazyloadedクラスに対して
opacity: 1;
とすることで、ロードされた瞬間を分かりやすくしています。

See the Pen IntersectionObserverのすゝめ by 葉っぱ一号 (@happa1go) on CodePen.

👆では画像遅延ロードを例に出しましたが、どんな処理を入れるかは発想次第で色々と考えられます。

例えば18行目、
if (entry.isIntersecting) 
の判定部分、👆では「この要素が画面に入ってきた場合」の処理のみ書いていますが、
else
を足せば、「この要素が画面から出て行った場合」の処理も加えることができます。

ターゲット要素が画面に真ん中あたりにきたらクラスを付加し、少し離れたらそのクラスを外す、というような処理を書けばスクロールを上下にするたびにタヌキが顔をだす、なんて感じにもできます。

See the Pen IntersectionObserverのすゝめ by 葉っぱ一号 (@happa1go) on CodePen.

重要なのは、IntersectionObserverは監視対象が表示された際にハイパフォーマンスでイベントを発生させてくれるということです。

さらに、オプションによって交差判定も便利にカスタマイズでき、これがまた使い勝手が良いのです。

というわけで、オプションを紹介しましょう。

オプション

オプションの説明はMDN Web Docsが分かりやすいので流用させてもらいます。というより概要はここ読めばだいたい分かります。

root

ターゲットが見えるかどうかを確認するためのビューポートとして使用される要素です。指定されなかった場合、または null の場合は既定でブラウザーのビューポートが使用されます。

枠を決める感じです。ブラウザーのビューポートを利用することが多いと思うので何も指定しない場合が多いでしょうか。たまに、ページ内のここのセクションを枠にしたい!って時がありますが、そういう時に指定します。

rootMargin

root の周りのマージンです。 CSS の margin プロパティに似た値を指定することができます。例えば、”10px 20px 30px 40px” (top, right, bottom, left) のようなものです。この値はパーセント値にすることができます。この一連の値は、交差を計算する前にルート要素の範囲のボックスの各辺を拡大または縮小させることができます。既定値はすべてゼロです。

個人的にはこれがすごく便利です。

画面に入ってから処理するのでは遅い! なんて時は
rootMargin: "100px 0px"
と設定すれば判定範囲が上下に100px拡大されたことになります。画面に入ってくる100px手前から処理を実行できるという訳です。
逆に画面に入ってしばらくしてから処理をしたい。なんて時には
rootMargin: "-20% 0%"
なんて感じにマイナスのマージンを設定すれば画面内に20%以上入ってきてから処理を実行できます。

次に説明するthresholdと組み合わせれば、要素が画面のど真ん中に来たらイベント発生! なんてこともできちゃいます。

threshold

単一の数値または数値の配列で、ターゲットがどのくらいの割合で見えている場合にオブザーバーのコールバックを実行するかを示します。見える範囲が 50% を超えたときのみ検出する場合は値 0.5 を使用します。 25% を超える度にコールバックを実行する場合は、 [0, 0.25, 0.5, 0.75, 1] という配列を指定します。既定値は 0 です(つまり、 1 ピクセルでも表示されるとコールバックが実行されます)。 1.0 の値は全てのピクセルが見えるようになるまで、閾値を超えたとはみなされないことを意味します。

すれっしゅほーるどぅ(ねいてぃぶ)。日本語的にはスレッショルド。
これすなわち「閾値(しきいち)」です。

こいつもまた便利なやつで、ターゲット自体がどのくらい表示されたときにイベント発生させたいかを指定できるのです。

が! 一つ注意すべき状況があります。それはrootよりもターゲットの方が大きい時です。

例えばrootの高さが1000pxなのに対し、ターゲットの高さが2000pxあったとすると、thresholdの値は0.5より大きい値をとることができません。

こんな状況で
threshold: 1.0
を設定していたりすると、いつまでたってもイベントが発生しない、なんてことになるわけです。

どんな処理をさせるかはあなた次第

ざっと説明してきましたが、ゆるーくまとめると、IntersectionObserverは要素の表示如何によるトリガーを提供してくれるAPIです。

IntersectionObserver出現以前にスクロールイベントの中でやっていたことを見てみると、本来の意味では「スクロールされたか?」ではなく、「コンテンツが表示されたか?」をトリガーとしていることが多く含まれていないでしょうか?

プログラミング全般に言えることだと思いますが、やりたいことの意味と、プログラム自体の意味は合致している方がなにかと良いものです。

「コンテンツが表示されたか?」というトリガーを純粋に提供してくれるものがなかったから、しぶしぶスクロールイベント内で疑似的な「コンテンツが表示されたか?」をつくっていたのなら、IntersectionObserverに移行する理由は十分です。

活用なき学問は、無学に等しい。

と諭吉も申しております。

まだIntersectionObserverを使用したことのない方はこの機会に試してみてはいかがでしょうか?

この記事を書いた人

葉っぱ一号

葉っぱ一号

フロントエンドエンジニア

おいしいお店を探すのが好きです。おいしいお店のために遠出もしちゃいます。遠出したい衝動のためにおいしいお店を探すのかもしれません。そんなものですよね。