書いて覚えるjQuery!画像の遅延ロード編

jQueryを使って、画像を遅延ロード(画像が表示されたタイミングで読み込むようにする)の作成方法を紹介します。画像の遅延ロードはLazy Load Pluginなどを使えば実現できますが、自分の手を動かして作成してみるとJavaScript、並びにjQueryの勉強になるかと思います。

今回のプログラムの動作確認は、Chrome(44.0.2403.107)とFirefox(39.0)で実施しました。

デモ

別ウィンドウで表示する場合はこちらをクリックしてください。

準備:ボタンを押したら画像をロード

まずは準備として、ボタンを押したら画像をロードするようにしてみます。これができると、後は「ボタンを押したら」「画像が表示されたら」に変えれば遅延ロードを実現できます。

別ウィンドウで表示する場合はこちらをクリックしてください。

ソース全体

HTML

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
    <script src="./base.js"></script>
  </head>
  <body>
    <img id="image" src="images/loading.png" alt="image" width="200" height="200" />
    <br />
    <button style="width: 180px; height: 32px;" id="load_button" type="button">画像読み込み</button>
  </body>
</html>

JS

$(function() {
  // 「画像読み込み」ボタンがクリックされた時の処理を定義しています。
  $('#load_button').click(function() {
    // 画像のsrc属性を上書きしています。
    $('#image').attr('src', 'images/foo1.png');
  });
});

ソース解説

画像をロード

// 画像のsrc属性を上書きしています。
$('#image').attr('src', 'images/foo1.png');

別の画像をロードしたい場合はsrc属性を上書きします。上記コードで「images/foo1.png」をロードし、元の画像を置き換えています。

実装1:画像が出現したら「appeared」イベントを実行

画像の遅延ロードを作成していきます。まずは、画像が出現したら「appeared」イベントを実行されるようにします。これは先日紹介したjquery.inviewなどを使えば実現できますが、今回は自作してみます。

ソース全体

$(function() {
  // 位置データを格納する連想配列を定義します。
  // key: imgタグが設置されている、画面上部からの位置
  // value: jQueryオブジェクト(imgタグ)を格納した配列
  var posData = null;
  var makePosData = function() {
    // 位置データを組み立てます。
    var posData = {}
    $.each($('img.lazyload'), function() {
      var $img = $(this);
      var pos = $img.offset().top;  // 画面上部からの位置を取得します。
      if (!(pos in posData)) {
        // posDataにキーがなければ空の配列を設定します。
        posData[pos] = [];
      }
      // 配列にimgオブジェクトを追加します。
      posData[pos].push($img);
    });
    return posData;
  }
  posData = makePosData();

  // ウィンドウがスクロールしたときなどの処理を定義します。
  // 処理内容は、「対象のimgタグが画面内に出現したらappearedイベントを発生」です。
  // ※「対象のimgタグ」は、posDataに含まれる、lazyloadクラスを持ったimgタグです。
  $(window).bind('load scroll resize', function() {
    // ブラウザの下端の位置を取得します。
    var windowBottom = $(this).scrollTop() + $(this).height();

    // posDataに含まれる位置でループします。
    var keys = Object.keys(posData);
    for (var idx1 = 0; idx1 < keys.length; idx1++) {
      var pos = keys[idx1];
      if (pos <= windowBottom) {
        // ブラウザの下端がimgタグの位置以上、つまりimgタグが画面に出現した場合

        // appearedを発生させる、imgタグの配列を取得します。
        var imgList = posData[pos];

        for (var idx2 = 0; idx2 < imgList.length; idx2++) {
          // appearedを発生させます。
          imgList[idx2].trigger('appeared');
        }
      }
    }
  });
});

ソース解説

位置データ組み立て

  var posData = null;
  var makePosData = function() {
    // 位置データを組み立てます。
    var posData = {}
    $.each($('img.lazyload'), function() {
      var $img = $(this);
      var pos = $img.offset().top;  // 画面上部からの位置を取得します。
      if (!(pos in posData)) {
        // posDataにキーがなければ空の配列を設定します。
        posData[pos] = [];
      }
      // 配列にimgオブジェクトを追加します。
      posData[pos].push($img);
    });
    return posData;
  }
  posData = makePosData();

画面を読み込んだタイミングで、画像オブジェクトと位置を紐付けたデータ(位置データ)を組み立てています。位置データは連想配列で、キーが「画像オブジェクトの画面上端からの位置」で、値が「画像(のjQuery)オブジェクト」です。

例えば、画面に(lazyloadクラスを持つ)画像が3つあり、それぞれ上端から50、100、150の位置にあれば、位置データは

位置データ {
  50: [画像オブジェクト1],
  100: [画像オブジェクト2],
  150: [画像オブジェクト3]
}

のようになります。

ちなみに、位置データの値を配列にしている理由は、同じ高さに画像が複数あった場合に対応するためです。

画像が画面に現われたかを判定

  $(window).bind('load scroll resize', function() {
    // ブラウザの下端の位置を取得します。
    var windowBottom = $(this).scrollTop() + $(this).height();

    // posDataに含まれる位置でループします。
    var keys = Object.keys(posData);
    for (var idx1 = 0; idx1 < keys.length; idx1++) {
      var pos = keys[idx1];
      if (pos <= windowBottom) {
        // ブラウザの下端がimgタグの位置以上、つまりimgタグが画面に出現した場合
        ...

画像が画面に現われたかを判定するために、まずは「ブラウザの下端の位置」を取得しています。そして、取得した「ブラウザの下端の位置」と「画像の位置」を比較して、「画像の位置」の方が上にある場合は「画像が画面に出現した」と判定します。

比較の際に使用する「画像の位置」は、先ほど組み立てた位置データのキーです。そのため、var keys = Object.keys(posData);でキーを取り出し、for文でループさせながら比較していきます。

「appeared」イベント実行

        // appearedを発生させる、imgタグの配列を取得します。
        var imgList = posData[pos];

        for (var idx2 = 0; idx2 < imgList.length; idx2++) {
          // appearedを発生させます。
          imgList[idx2].trigger('appeared');
        }

triggerメソッドを使って、appearedイベントを実行しています。なお、位置データの値は配列なので、ここでもfor文でループさせつつtriggerを実行していきます。

実装2:「appeared」を受けて画像を置換

「実装1:画像が出現したらappearedイベントを実行」まで完成したら、後は「準備:ボタンを押したら画像をロード」で作成したように、src属性を置き換えることで画像の遅延ロードを実現できます。

ソース全体

$(function() {
  ...

  // appearedが発生されたときに実行する処理を定義します。
  // 処理内容は、「imgタグのsrcを置き換える」です。
  $('img.lazyload').on('appeared', function() {
    var $img = $(this);  // 処理対象のimgタグのオブジェクト

    // 既に画像をロード済みかを判定します。
    if ($img.attr('data-lazyloaded') === undefined) {
      // まだ画像をロードしていない場合は、srcをdata-srcで置き換えます。
      // setTimeoutは、画像が読み込まれていることをわかりやすくするために使っています。
      // これにより、「画像に出現」から「画像読み込み」までに500ミリ秒だけ遅らせています。
      setTimeout(function() {
        $img.attr('src', $img.attr('data-src'));
      }, 500);

      // 画像ロード済みを示す属性を設定します。
      $img.attr('data-lazyloaded', true);
    }
  });
});

ソース解説

「appeared」イベント発生時の処理を定義

先ほどのコードで、画像が画面に出現したら「appeaered」イベントを実行していました。

          // appearedイベント実行部分
          imgList[idx2].trigger('appeared');

このイベントが発生したときに実行する処理は、onを使って以下のようにして描くことができます。

  $('img.lazyload').on('appeared', function() {
    ...
  });

これで、「画像が画面に出現したら」「...」に記述した内容が実行されるようになります。

data-lazyloadedの判定・設定

    var $img = $(this);  // 処理対象のimgタグのオブジェクト

    // 既に画像をロード済みかを判定します。
    if ($img.attr('data-lazyloaded') === undefined) {
      ...
      // 画像ロード済みを示す属性を設定します。
      $img.attr('data-lazyloaded', true);
    }

画像を遅延ロードした後、再度画像が出現したときに再びロードしないようにしています。

setTimeoutの使用

      setTimeout(function() {
        $img.attr('src', $img.attr('data-src'));
      }, 500);

画像のロードはattr属性を変更するだけです。ただ、画面に出現した瞬間に画像が置き換わったら動作がわかりにくいので、setTimeoutを使ってタイムラグを設けています。

備考

位置データの再構築について

今回のサンプルではページ読み込み時に、画像の位置データを組み立てています。そして、その情報に基づき画像が出現したかを判断しています。

ただ、この方法だと、ページ読み込み後に画像の位置が変わる場合に対応することができません。その場合は、位置データを再構築する必要があります。

例えばブラウザをリサイズしたときに位置データを再構築するには、以下のようになります。

  $(window).on('resize', function() {
    posData = makePosData();
  });

遅延ロードのソース全体

遅延ロードのソース全体は以下の通りです。また、GitHub上でも確認できます。

index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
    <script src="./base2.js"></script>
  </head>
  <body>
    <ul style="list-style-type: none;">
      <li><img class="lazyload" src="images/loading.png" data-src="images/foo1.png" alt="image" width="200" height="200" /></li>
      <li><img class="lazyload" src="images/loading.png" data-src="images/foo2.png" alt="image" width="200" height="200" /></li>
      <li><img class="lazyload" src="images/loading.png" data-src="images/foo3.png" alt="image" width="200" height="200" /></li>
      <li><img class="lazyload" src="images/loading.png" data-src="images/foo4.png" alt="image" width="200" height="200" /></li>
      <li><img class="lazyload" src="images/loading.png" data-src="images/foo5.png" alt="image" width="200" height="200" /></li>
    </ul>
  </body>
</html>

base.js

$(function() {
  // 位置データを格納する連想配列を定義します。
  // key: imgタグが設置されている、画面上部からの位置
  // value: jQueryオブジェクト(imgタグ)を格納した配列
  var posData = null;
  var makePosData = function() {
    // 位置データを組み立てます。
    var posData = {}
    $.each($('img.lazyload'), function() {
      var $img = $(this);
      var pos = $img.offset().top;  // 画面上部からの位置を取得します。
      if (!(pos in posData)) {
        // posDataにキーがなければ空の配列を設定します。
        posData[pos] = [];
      }
      // 配列にimgオブジェクトを追加します。
      posData[pos].push($img);
    });
    return posData;
  }
  posData = makePosData();

  // ウィンドウがスクロールしたときなどの処理を定義します。
  // 処理内容は、「対象のimgタグが画面内に出現したらappearedイベントを発生」です。
  // ※「対象のimgタグ」は、posDataに含まれる、lazyloadクラスを持ったimgタグです。
  $(window).bind('load scroll resize', function() {
    // ブラウザの下端の位置を取得します。
    var windowBottom = $(this).scrollTop() + $(this).height();

    // posDataに含まれる位置でループします。
    var keys = Object.keys(posData);
    for (var idx1 = 0; idx1 < keys.length; idx1++) {
      var pos = keys[idx1];
      if (pos <= windowBottom) {
        // ブラウザの下端がimgタグの位置以上、つまりimgタグが画面に出現した場合

        // appearedを発生させる、imgタグの配列を取得します。
        var imgList = posData[pos];

        for (var idx2 = 0; idx2 < imgList.length; idx2++) {
          // appearedを発生させます。
          imgList[idx2].trigger('appeared');
        }
      }
    }
  });

  // appearedが発生されたときに実行する処理を定義します。
  // 処理内容は、「imgタグのsrcを置き換える」です。
  $('img.lazyload').on('appeared', function() {
    var $img = $(this);  // 処理対象のimgタグのオブジェクト

    // 既に画像をロード済みかを判定します。
    if ($img.attr('data-lazyloaded') === undefined) {
      // まだ画像をロードしていない場合は、srcをdata-srcで置き換えます。
      // setTimeoutは、画像が読み込まれていることをわかりやすくするために使っています。
      // これにより、「画像に出現」から「画像読み込み」までに500ミリ秒だけ遅らせています。
      setTimeout(function() {
        $img.attr('src', $img.attr('data-src'));
      }, 500);

      // 画像ロード済みを示す属性を設定します。
      $img.attr('data-lazyloaded', true);
    }
  });
});

この記事が役に立った場合、シェアしていただけると励みになります!!