サムネイル画像を遅延読み込みでオリジナルに変更

この1,2ヶ月、モバイルユーザビリティのエラー対策に悩まされているのですが、そうした作業の中からもいろいろ学ぶことが多く、そう悪いものではありません(笑涙)。


www.imuza.com



イメージの遅延読み込み

そのひとつがイメージの遅延読み込みです。

随分前から Lazy Load という jQuery のプラグインがあることは知っており使ったこともあるのですが、高速回線が一般的になってきた(かな?)こともありあまり意識しなくなっていました。しかし、今回のモバイルユーザビリティのエラーで、Google がウェブの読み込み速度をかなり重要視していることがわかり、イメージの遅延読み込みの方法として Intersection Observer というものを知りました。


developer.mozilla.org


画像素材サイトなどでスクロールすると次々に画像が表示されるサイトがありますが、そうしたサイトで使われている手法のようです。スクロールイベントで実装しているわけではないようです。


Google 推奨の遅延読み込み

developers.google.com


まだ Intersection Observer を理解しきれていませんので、私がつまらぬ説明をするよりも上のサイトをご覧ください。サンプルコードも記載されています。


HTML サンプルコード

<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load-1x.jpg" data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x" alt="I'm an image!">


イメージ要素に lazy クラスを指定し、src 属性にはダミーを書いておき、カスタムデータ属性に画像 URL を指定しておくということです。


Javascript サンプルコード

document.addEventListener("DOMContentLoaded", function() {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } else {
    // Possibly fall back to a more compatible method here
  }
});


DOM の構築が完了したら lazy クラスを持つ画像に IntersectionObserver を設定(ここまだよくわかっていない)して監視するようにしておき、その画像がビューポートに入った時に画像を読み込み表示するということです。

画像の差し替えはスクロールイベントを使う場合と同じようです。ただ、スクロールイベントの監視は負荷が高いですので好ましくないということでしょう。

IE には対応していませんが、polyfill するライブラリーやスクロールイベントを使った代替コードも記載されています。


Can I use... Support tables for HTML5, CSS3, etc


はてなブログのサムネイルに応用する

これをはてなブログの通常の画像に応用するには

<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load-1x.jpg" data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x" alt="I'm an image!">

というサンプルのように HTML で画像部分を書くことで対応できると思います。まあ、面倒ではあります。


で、ふと思いつき、はてなブログのサイドバーに挿入するモジュールのサムネイル画像をオリジナルに差し替えることに使えないかとやってみました。


document.addEventListener("DOMContentLoaded", function() {
    const getOrgImg = function(arg){
        const re = /https?%3A%2F%2F.+\.(jpg|jpeg|gif|png|bmp)/i;
        const imageUri = re.exec(arg);
        return decodeURIComponent(imageUri[0]);
    };

    var lazyImages = [].slice.call(document.querySelectorAll("img.urllist-image"));
    if ("IntersectionObserver" in window) {
        let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
            entries.forEach(function(entry) {
                if (entry.isIntersecting) {
                    let lazyImage = entry.target;
                    let imageUri = lazyImage.src;
                    lazyImage.src = getOrgImg(imageUri);
                    lazyImageObserver.unobserve(lazyImage);
                }
            });
        });
        lazyImages.forEach(function(lazyImage) {
            lazyImageObserver.observe(lazyImage);
        });
    } else {
        // Intersection Observer 非対応の場合
        let active = false;
        const lazyLoad = function() {
            if (active === false) {
                active = true;
    
                setTimeout(function() {
                    lazyImages.forEach(function(lazyImage) {
                        if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
                            let imageUri = lazyImage.src;
                            lazyImage.src = getOrgImg(imageUri);
                            lazyImages = lazyImages.filter(function(image) {
                                return image !== lazyImage;
                            });
    
                            if (lazyImages.length === 0) {
                                document.removeEventListener("scroll", lazyLoad);
                                window.removeEventListener("resize", lazyLoad);
                                window.removeEventListener("orientationchange", lazyLoad);
                            }
                        }
                    });

                    active = false;
                }, 200);
            }
        };
 
        document.addEventListener("scroll", lazyLoad);
        window.addEventListener("resize", lazyLoad);
        window.addEventListener("orientationchange", lazyLoad);
    }
});


これでいけます。

今モバイルユーサビリティの件でいろいろやっているサイトではトップページの「おすすめ映画」や「注目記事(よく読まれている記事)」の表示に利用しています。


www.movieimpressions.com


ただし、「注目記事」はリアルタイムで表示するように ajax を使っているようですので、DOMContentLoaded ではまだ画像要素自体が作成されていません。現在のところ、load イベントを使っていますが、load イベントの発火は ajax の完了を待つわけではないと思いますのでどうしたらいいのか思案中です。

なお、この遅延読み込みによってモバイルユーザビリティのエラーが解消したわけではありません。