Written by TSUYOSHI

Intersection Observer で画像の遅延ロードを実装する

JavaScript PROGRAMMING

JavaScript初心者の人「スクロールする処理などにIntersection Observerを使うとよいと聞いたことがあるが、よくわからない」

こういった疑問にお答えします。

本記事では、JavaScript初心者の方向けにIntersection Observer について解説します。JavaScriptを使う内容になります。

本記事の内容

  • Intersection Observerの概要
  • Intersection Observerの実装方法
  • オプションについて
  • 監視対象を設定する
  • 監視を解除する
  • 実例1〜3

本記事を書いている僕はフロントエンドの現役エンジニアです。
ソースコードを交えて解説していて、ローカルに落として自分で簡単に試すことができるので初めての方でも理解できるかと思います。

Intersection Observerの概要

Intersection Observer はJavaScriptを使って書きます。設定をすると、要素と要素が交差するタイミングでコールバックを呼ぶことができ、ここに処理を入れるということになります。
例えば、縦長のコンテンツページにおいて、ファーストビューでは入らないページ下部のコンテンツについて、スクロールで画面内に入って来た時に何かしら処理をしたい時には、交差する要素をビューポート(表示画面)にすることによってこれを実現できます。

Intersection Observerで書くほうがよい理由

ページの速度改善で、表示画面外の画像は画面に入るタイミングで読み込ませる画像の遅延ロード処理などがよくありますが、この時にどのように実現させているでしょうか。多くの場合はscrollTopで現在表示位置を確認したり、scrollイベントで表示の判定をして画像の遅延ロードをさせたりしているのではないかと思います。
このような処理において、Intersection Observer を使えば簡潔に処理を書くことができ、尚且、SEOにも有利になります。

以前「WordPressのページ読み込み速度を上げるプラグイン「Native Lazyload」という記事でも少し取り上げていますが、Googleでも「IntersectionObserver API とポリフィルを使用する」という方法が挙げられています。

Intersection Observerの実装方法

Intersection Observerの詳細はMDN などで詳細な仕様を確認することができます。設定は非常に簡単で、基本的な書き方は以下のとおりです。順を追って説明していくので、ざっと読み流して、後述の実例を見ていただければわかりやすいかもしれません。

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);

オプションについて

new IntersectionObserver」でコンストラクタを呼び出す際に、コールバックとオプションを指定するだけで、オプションは指定しなければデフォルト設定になります。

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}
  • root
    「ターゲットが見えるかどうかを確認するためのビューポートとして使用される要素」であり、デフォルトでブラウザーのビューポート(つまり表示画面)となります。モーダル表示内で使う場合などはここにモーダルのwrapperなどを指定することになるかと思います。
  • rootMargin
    rootのマージン領域になります。CSSのmarginと同じように設定でき、「”10px 20px 30px 40px” (top, right, bottom, left) 」のように設定します。ここで設定したマージン領域分、rootの交差する領域が広がることになります。デフォルトではmarginはすべてゼロになっています。
  • threshold
    交わる要素とどれくらい交わったらコールバックを呼ぶかの数値で、0.01.0で設定します。要素の交差が50%でコールバック実行したい場合は「0.5」を設定します。

監視対象を設定する

監視する対象を「observe」メソッドで登録します。

var observer = new IntersectionObserver(callback);
observer.observe(document.getElementById("elementToObserve"));

監視を解除する

監視している要素を監視から外したい時は「unobserve」メソッドで解除します。1度きりの処理で、コールバックで処理が1度完了したら、次からはコールバック実行を発生させたくない場合などに使います。

observer.unobserve(document.getElementById("elementToObserve"));

1.Intersection Observerの基礎

githubに上げている実際の例を見ていきます。よろしければ実際にダウンロードしてみて試してみてください。example1〜3の3つに分けて、実例を順に解説していきます。
github: https://github.com/it-web-life/js_intersection_observer

example1: スクロールして画面に入ったら、赤いブロックを消していく

<section class="hide block-item js-intersection-load">
  <p>Change Color Area 1</p>
</section>

10個のブロック(.block-item)が縦長に並んでいるページで、デフォルトで「hide」 classが付いていて、ブロックのbackgorund-colorが赤くなるようにしています。
表示画面(ビューポート)に入ったら、この「hide」 classを削除して、背景を白にします。
※切り替えがわかりやすいようにtransitionで0.5秒かけて色が変わるようにしています。

つまり監視対象となる要素にはすべて「js-intersection-load」classを付けていて、画面と交差したら(画面に表示されたら)hideclassremoveして消していきます。

▼Intersection Observerソースコードの解説

const observerParts = new IntersectionObserver((entries) => {
  console.log('%c---observer entries コールバック---', 'color: red;', entries);

  // 監視のコールバック処理
  entries.forEach((entry) => {
    console.log('intersection observer 呼び出し');

    console.log('isIntersecting', entry.isIntersecting);

    // 交差している場合
    if (entry.isIntersecting) {
      console.log('読み込み!');

      // 表示処理
      entry.target.classList.remove('hide');

      // 監視を解除する
      observerParts.unobserve(entry.target);
    }
  });
});

// 監視する要素を取得
const partsTargets = [...document.getElementsByClassName('js-intersection-load')];

// 監視する要素をIntersection Observer登録
partsTargets.forEach((target) => observerParts.observe(target));

最初に「new IntersectionObserver」で初期化して第一引数にコールバックを設定しています。次に
const partsTargets = […document.getElementsByClassName(‘js-intersection-load’)];
で今回監視する「js-intersection-load」classが付いている要素をすべて取得します。
そしてその要素すべてを、
partsTargets.forEach((target) => observerParts.observe(target));
の部分で「observe」メソッドで登録していきます。
画面に表示された時にコールバックが呼び出されます。
コールバックでは「entries」を受け取り、console.logに出していますが、

  • boundingClientRect
  • intersectionRatio
  • intersectionRect
  • isIntersecting
  • isVisible
  • rootBounds
  • target
  • time

などのプロパティがあります。

今回の解説では以下を使います。

  • isIntersecting」 rootとターゲットが交差しているかどうか
  • target」 ターゲットとしている監視要素

コールバックが実行されたら、今回はentriesで引数として受け取っている中に、監視要素が配列で入っているので、各要素を「entry.isIntersecting」の部分で交差しているか判定します。
rootと交差していれば、isIntersectingプロパティはtrueとなり、その際に今回は「entry.target.classList.remove(‘hide’);」として、ターゲット要素から「hide」classを削除しています。
そして、「unobserve」メソッドでIntersection Observer監視を解除しています。
ここで解除しないと何度も交差のタイミングでコールバックが実行され、無駄な処理が発生することになるので、今回はこの場所で解除処理をいれています。

2.Intersection Observer オプション設定の例

次にオプションを設定したexample2を解説します。html, cssは同じでjsのみ違います。

const isIntersectionObserver = !!('IntersectionObserver' in window);
console.log('isIntersectionObserver', isIntersectionObserver)

const options = {
  // 1画面分の高さマージンをとる
  rootMargin: `${window.innerHeight}px 0px`
};

console.log('options', options);

// IntersectionObserver処理
const observerExec = () => {
  // ここの処理の前にisIntersectionObserverで判定をつけて、trueなら以下を実行、falseなら既存処理を行うようわける
  const observerParts = new IntersectionObserver((entries) => {
    console.log('%c---observer entries コールバック---', 'color: red;', entries);

    // 監視のコールバック処理
    entries.forEach((entry) => {
      console.log('intersection observer 呼び出し');

      console.log('isIntersecting', entry.isIntersecting);

      // 交差している場合
      if (entry.isIntersecting) {
        console.log('読み込み!');

        // 表示処理
        entry.target.classList.remove('hide');

        // 監視を解除する
        observerParts.unobserve(entry.target);
      }
    });
  }, options);

  // 監視する要素を取得
  const partsTargets = [...document.getElementsByClassName('js-intersection-load')];

  // 監視する要素をIntersection Observer登録
  partsTargets.forEach((target) => observerParts.observe(target));
};

// Intersection Observerが有効なブラウザのみ処理する (非対応ブラウザは別途ポリフィル導入や代替対応などが必要)
if (isIntersectionObserver) {
  observerExec();
}

example1とほぼ同じで、単純にoptionを追加しただけです。
オプションには、root, rootMargin, thresold が設定できますが、今回はrootMarginのみを設定しています。
縦に1画面分(window.innerHeight)マージンを取っているので、スクロールしている際に現在表示している画面の更に1画面下まで、先に読み込みするようにしています。画面に表示されてからでは処理が遅いといった場合などに使う想定です。ChromeDevToolsなどで現在表示されている画面とその下の部分がどうなっているかを確認するとよくわかると思います。
また冒頭の「const isIntersectionObserver = !!(‘IntersectionObserver’ in window);」にて、そのブラウザでIntersection Observerが有効化どうかを判定しています。IEなどではIntersection Observerは使えないため、ポリフィルを入れるか、scrollで処理するように処理をわけるなどの必要があります

3.画像の遅延ロード処理をIntersection Observerで実装する

example3に画像の遅延ロードをする処理を入れてみました。スクロールして画面内にブロックが入ったら、画像を表示するようにしています。js以外はほぼ同じです。(表示がわかりやすいようにCSSだけ少しtransitionやopacityの変更をしています)
オプションで1画面先まで読み込むようにしていて、交差領域(1画面下まで)に入ると約2秒かけて画像を表示するようになっています。

const options = {
  // 1画面分の高さマージンをとって先読み込みする
  rootMargin: `${window.innerHeight}px 0px`
};

console.log('options', options);

// Intersection Observer処理
const observerParts = new IntersectionObserver((entries) => {
  console.log('%c---observer entries コールバック---', 'color: red;', entries);

  // 監視のコールバック処理
  entries.forEach((entry) => {
    console.log('intersection observer 呼び出し');

    console.log('isIntersecting', entry.isIntersecting);

    // 交差している場合
    if (entry.isIntersecting) {
      console.log('読み込み!');

      // 画像を取得
      const imageElement = [...entry.target.getElementsByTagName('img')];

      // 画像があるか確認
      if (!imageElement.length) return;

      // 画像を表示する
      const imageUrl = imageElement[0].getAttribute('data-src');
      imageElement[0].setAttribute('src', imageUrl);
      // 視認できるようにCSSのopacity制御もかけています
      entry.target.classList.remove('hide');

      // 監視を解除する
      observerParts.unobserve(entry.target);
    }
  });
}, options);

// 監視する要素を取得
const partsTargets = [...document.getElementsByClassName('js-intersection-load')];

// 監視する要素をIntersection Observer登録
partsTargets.forEach((target) => observerParts.observe(target));

ページを読み込んだ後、勢いよくスクロールしていけば、3画面分下くらいから画像が遅れて表示されるのがわかると思います。
rootMarginで1画面分多く交差領域を設定しているので、最初の1画面目とさらにもう1画面分の2画面分にある画像が最初に読み込まれています。スクロールするとさらに下の画面が先読みされていくという仕組みです。
ちなみにIntersection Observerに対応していないブラウザは考慮して作っていないので、ポリフィルを入れるなどの考慮が必要です。

以上、Intersection Observerの解説でした。初学者のお役に立てれば幸いです。

※当サイトでは一部のリンクについてアフィリエイトプログラムを利用して商品を紹介しています