IntersectionObserver でオフスクリーンの画像を遅延読み込みする

Posted on 2018/12/03 in tech

Google の PageSpeed Insights が新しくなっていた。

で、改善案の一つに Offscreen Images のセクションがあったのだけれど、こう書いてあった。

Consider using an IntersectionObserver to intelligently determine when to lazy-load offscreen images. For example, suppose you have some images at the bottom of a very long page. With an IntersectionObserver, you can load the images only when the user has scrolled halfway down the page. See Intersect all the things! for more on this approach.

意訳:

オフスクリーンの画像をいい感じに遅延読み込みできる IntersectionObserver を使うことを考えてください。例えば、とても長いページの底あたりに画像があると仮定します。IntersectionObserver を使うと、ユーザが途中までスクロールしたときだけ画像を読み込むことができます。より知りたければ Intersect all the things! のページへ。

ただ、肝心の IntersectionObserver のページには基本的なチュートリアルしかない。つまり遅延ロード(オフスクリーン画像の遅延読み込み)の実装には触れていない。使ってみてねとは言ったけどやり方を教えるとは言っていない。

というわけで、自分なりに実装してみました。

とりあえずコード

実際に遅延ロードを実装したものを見ていきます。

まず img タグでは

<img src="load.gif" data-origsrc="/img/foot_logo.png"/>
  • 実際に読み込みたい画像(通常 src 属性に指定する)を data-origsrc に指定
  • src には代わりに表示する gif 画像を指定

といったことを行っておきます。gif 画像はなくてもいいです。

そんでもって javascript をこんな感じに実装する。

<!-- polyfill を読み込む -->
<script src="/js/intersection-observer.min.js"></script>
<script>
window.addEventListener('DOMContentLoaded', function() {
  var io = new IntersectionObserver(
      function(entries) {
         // observe しているすべての要素は entries から取得できる
         entries.forEach(function(entry) {

            // intersectionRatio には、entry (要素)がどれだけビューに現れているかの
            // 割合が 0~1 の範囲で入る。
            // 0 だとまったく表示されていない。
            // 1 だとすべて表示されている
            if(entry.intersectionRatio == 0) { continue }; // 要素がビューに現れていないのでスキップ

            // entry.target から observe した要素を参照できる
            // `data-origsrc` の値を `src` にセットし、画像を読み込む
            entry.target.src = entry.target.dataset.origsrc;

            // 読み込み済みのは監視不要なので unobserve で observe 解除する
            this.unobserve(entry.target);
         });
      },
      {
         /** ↓デフォルト値のオプションをあえて書いてます **/
         // ビューポートとする要素。指定しなければ viewport
         // ovserve する対象がここで指定したビューポートと交差したときに
         // コールバックが呼ばれる
         root: null,

         // root に対するマージン(root の範囲がこれにより拡大・縮小する)。css の`margin` のように指定する
         // パーセント単位を使いたいときは root を明示する。
         rootMargin: "0px 0px 0px 0px",

         // 監視対象がどれだけビューポートと交差したタイミングで
         // コールバックを呼ぶかを配列で指定する。
         // 例えば、`0, 0.25, 0.5` だと、要素がそれぞれ
         // 0%, 25%, 50% 表示されたタイミングでコールバックが呼ばれることになる
         threshold: [0],
      }
   );

  // このサンプルではすべての img タグを遅延ロードの対象としている
  var imgs = document.querySelectorAll('img');
  for(var i = 0;i < imgs.length;i++) {
    var img = imgs[i];

    // data-origsrc 属性のない要素はスキップ
    if(!img.dataset.origsrc) { continue; }

    // observe 対象の要素(target)を追加
    io.observe(img);
  }
})
</script>

polyfill

IntersectionObserver は IE, Edge, Safari では対応していないので、polyfill を読み込ませましょう。

<script src="/js/intersection-observer.min.js"></script>

ざっくり解説

IntersectionObserver のコンストラクタ呼び出し用引数の第 1 引数には、 要素がビューに現れたときに呼び出すコールバックを渡す。

var io = new IntersectionObserver(function(entries) {
   // ここの処理が呼ばれるよ
   console.log("I'm appeared !");
});
var io = new IntersectionObserver(function(entries) {
  ...
  },
  {
    'root': document.getElementById('scrollRoot'),
    'rootMargin': '5px',
    'threshold': [0, 0.5, 1],
  }
);

第 2 引数はオプション用のオブジェクトを渡す。

root

スクロールする要素を指定する。この要素内の要素が、 親要素上で出現 したときがいわゆる Intersection (交差)したときに相当し、後述の thread オプションの値に応じてコールバックが呼び出される。

指定しない場合は viewport になる。

rootMargin

root に対するマージン。rootrootMargin のマージンによって拡大・縮小した範囲が後述の threshold のトリガ判定に使われる

threshold

root 上に observe 対象がどれだけ表示されたタイミングでコールバックを呼び出すか配列で設定する。


サンプルコードのコメントも参考にしていただけたら。

遅延ロードの効果

画像の多いサイトだとスコアがこれだけで 15点 ほど上がった。jQuery の lazyload だと、 ページ途中の画像なんかだと不具合があったりして Google の評価がそこまで上がらなかったので、 これは結構いいのでは。

まとめ

IntersectionObserver つよい。パフォーマンスもいいらしい。ただ、polyfill が当分は必要そう。

参考


動くサンプルコード書きたかった。他にも書きたいネタがあるのでこのへんで。。