バックエンドから返却される規定画像要素に対してLazyload(遅延読み込み)をフロントエンドだけで実現させる

Lazyload(遅延読み込み)とは

例えば、大量の画像の読み込みが必要なページの場合、大量の画像の読み込みはページレンダリングのボトルネックになりやすく、ページの初期表示の遅延の原因となることが多い。こういったページの初期表示の高速化につなげる手法として「Lazyload(遅延読み込み)」が使用される。

画像だけではなくインラインフレーム、動画やスクリプトも遅延読み込みの対象となることがあるが、一般的な遅延読み込みの対象は画像(<img>要素で使用される画像)である。<img>要素を遅延読み込みする場合、それらがビューポート内に入った際、src 属性を有効化する手法がよく取られる。

実施条件(縛り)

  • バックエンド側からのレスポンスで<img>要素にsrc`属性は設定されている
  • img 要素の出力場所はテンプレートコーディングで自由な場所に記述できる
  • ネイティブ Lazyload(loading=lazy)は使用しない

イメージ

<!-- テンプレート -->
<body>
  <%= IMG_SRC_OUTPUT %>
</body>

<!-- ↓↓↓↓↓ -->

<!-- 出力 -->
<body>
  <img src="./sugoi.png" />
</body>

実装

まず、「遅延読み込み」の要である画像のリクエストが発生しないようにしなければならない。

クライアントへのレスポンスの時点で<img src="〜">が返ってくることが前提であるので、下記のように<template>要素を包括させる。

<!-- テンプレート -->
<body>
  <template> <%= IMG_SRC_OUTPUT %> </template>
</body>

<!-- ↓↓↓↓↓ -->

<!-- 出力 -->
<body>
  <template>
    <img src="./sugoi.png" />
  </template>
</body>

こうすることで<img>要素はレンダリングされず且つ画像の読み込みが発生しないようになる。

template 要素の特徴

  1. <template>内のコンテンツはレンダリングされない

  2. <template>内のコンテンツは反応しない:
    → 画像の読み込みは発生しない <img>

  3. <template>内のコンテンツは DOM ツリー状に存在しないため、document.getElementById()querySelector() で取得することが出来ない

Lazyload の処理を設定する

ここからが JavaScript(クライアントサイド)の出番。

大まかに下記のような流れになる:

  1. <template>のコンテンツから<img>要素を取り出す
  2. 取り出した<img>要素のsrcを data 属性へ置き換える
  3. 処理した<img>要素に対して Lazyload の処理を加える
  4. <img>要素をdocumentへ追加する

template 要素から img 要素を取り出す

<template class="js-template-element">
  <img src="./sugoi.png" />
  <img src="./sugosugi.png" />
  <img src="./super-sugoi.png" />
  <img src="./sugokunai.png" />
</template>
// <template>要素から取得
const template = document.querySelector(".js-template-element");
const contents = document.importNode(template.content, true);
const images = contents.querySelectorAll("img");

srcを data 属性へ退避する

<template>要素から取り出した状態のままdocumentへ追加してしまうと画像読み込みが起きてしまうため、<img>要素のsrcを data 属性(data-srcとする)へ退避させる。

const strData64gif =
  "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
const images = contents.querySelectorAll("img");

for (let i = 0; i < images.length; i++) {
  const image = images[i];
  const src = image.getAttribute("src");

  image.setAttribute("data-src", src);
  image.setAttribute("src", strData64gif);
}

img要素の src 属性は必須かつ空 ("") または null である場合にエラーが発生してしまう。また、Firefox などでは、「壊れた画像」として扱われ、縦横サイズを指定してもサイズが保たれない。そのため、「Data URI(データ URI スキーム)」で「透過 gif」を src 属性にセットしておく。

処理した<img>要素に対して Lazyload の処理を加える

Lazyload の手法はいくつかあるが、Intersection Observer API(交差監視 API)を利用する。

const lazyImages = document.querySelectorAll("img[data-src]");

let lazyImageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const lazyImage = entry.target;

      lazyImage.src = lazyImage.dataset.src;
      lazyImageObserver.unobserve(lazyImage);
    }
  });
});

for (let i = 0; i < lazyImages.length; i++) {
  const lazyImage = lazyImages[i];
  lazyImageObserver.observe(lazyImage);
}

対象の<img>要素がビューポートに入るとdata-srcに退避した URL をsrcへ追加するため、画像の読み込みが開始される。

要素を doument へ追加する

メモリ上に存在していた要素をdocumentへ追加しレンダリングさせる。

document.body.appendChild(images);

快適な Lazyload ライフを。

参考