Analog Studio

超軽量コードで画像の遅延読込を組み込む方法 (542B)【jQuery不要】

概要

平成最後の GW は10連休のようで、ダラダラと過ごしていますでしょうか?
時間が出来たら、ちょっとずつやればいいやと思いなかなか手につかないことも多いでしょう。

Web の世界にも時が来れば・必要になれば実行しよう、という怠惰な動作をするものがあります。
それが「Lazy Load」(遅延読込) という画像などが表示領域内にスクロールされてから読み込む技術です。

色々なライブラリが公開されていますが、カスタムデータ属性 (任意に設定できる data-*** という HTML 要素に付ける属性) が必要です。
新規で構築する場合は良いですが、既に作ってしまったページに適応するのはかなり面倒です。
そこで、カスタムデータ属性を使わないで既存ページに追加するだけで使える超軽量コード (542B) を作ってみました。
そこそこ汎用性があると思いますので、是非ご検討下さい。

LazyLoad・遅延読込とは?

Lazy Load (遅延読込) とは、画像などの容量が大きな要素を画面構成に必要な分だけ都度読み込ませる、という技術です。
表示領域外の画像を読み込む必要がないので高速な読込にも寄与します。

Web ページには多くの画像が使用されおり、通常はスクロールされるまで表示されない画像も全て読み込まれるので通信負荷が高くなります。
仮にファーストビュー以外はスクロールせずに離脱した場合、表示されなかった画像の読み込みは全く無駄になります。
これはユーザにはもちろん、サーバへの負荷という意味でもムダしかありません。

通常はスクロールされるまで表示されない画像まで読み込む必要がある

そこで、Javascript を使って表示していない画像の読み込みを後回しにしてやろうというわけです。
ユーザにとってはページが直ぐに表示されますし、サーバへの負荷も減ります。

但し、スクロールして表示する必要が出てきたタイミングで画像をロードしに行くので多少の表示遅れは発生します。
ファーストビューは早くなりますが、以降はスクロールした時点で読み込み始めるのでそこはご注意下さい。
この遅れが許容できない場合にはファーストビューの表示速さと天秤に掛ける必要があります。

遅延読込を実行させるJavascript

というわけで、早速コードを確認しましょう。見ての通り非常にシンプルです。
ダミー画像込みで、変数名などをこのままでも不要なスペース・コメントを削除すればすれば 817B になります。
(strict モードでエラーを出さずにショートコード化すれば 542B になりました)

// 全ての画像を1px四方の透明GIFに置き換える const lazyLoadSet = function(){ // <IMG>要素のsrc属性値を変数に保管しておく let data = new Array(); const imgs = document.getElementsByTagName("img"); for(let i = 0, n = imgs.length; i < n; i++){ // データを変数に保管する data.push({URL: imgs[i].src, selector: imgs[i]}); // 透明GIFに置き換える (BASE64エンコードした画像を使う) imgs[i].src = "data:image/gif;base64,R0lGODlhAQABAGAAACH5BAEKAP8ALAAAAAABAAEAAAgEAP8FBAA7"; } // <IMG>要素以外の読み込みが終わったら遅延読込を始める window.addEventListener("load", function(){ // 遅延読込を呼び出す // data変数には読み込んでいない画像の配列が戻る data = lazyLoadRun(data, 750); // スクロールイベントを追加する // スクロールすると順次画像が読み込まれる let timerID = null; window.addEventListener("scroll", function(){ if(!timerID){ timerID = setTimeout(function(){ timerID = null; data = lazyLoadRun(data, 500); if(data.length < 1){ timerID = true; } // コンソールに残り画像数を表示させる console.log(data.length); }, 200); } }, false); }, false); // 画像の読込処理関数 const lazyLoadRun = function($data, $pre){ const returnArr = new Array(); const windowHeight = document.documentElement.clientHeight; for(let j = 0, m = $data.length; j < m; j++){ if($data[j].selector.getBoundingClientRect().top < windowHeight + $pre){ $data[j].selector.src = $data[j].URL; } else{ returnArr.push($data[j]); } } return returnArr; }; }; // DOM構築が完了したらLazyLoadの準備を行う // <BODY>の一番最後にこのコードを置く場合は即時関数にして以下を削除してもOK window.addEventListener("DOMContentLoaded", lazyLoadSet, false);

実行結果

デモページを用意しましたのでコンソールを開いて状態を確認してみて下さい。

Lazy Load デモページ

コードの解説

簡単にコードの解説をしていきます。
改造する際のヒントなども紹介します。

全ての画像を透明GIFに置き換える

まずは、全ての画像の読み込みを中断させる為に透明 GIF に置き換えてしまいます。
透明 GIF は一々用意しなくて済むように BASE64 エンコードした Data URL を使います。

この置き換えのタイミングで画像 URL が失われないように変数に収めておきます。

// <IMG>要素のsrc属性値を変数に保管しておく let data = new Array(); const imgs = document.getElementsByTagName("img"); for(let i = 0, n = imgs.length; i < n; i++){ // データを変数に保管する data.push({URL: imgs[i].src, selector: imgs[i]}); // 透明GIFに置き換える (BASE64エンコードした画像を使う) imgs[i].src = "data:image/gif;base64,R0lGODlhAQABAGAAACH5BAEKAP8ALAAAAAABAAEAAAgEAP8FBAA7"; }

まずは、画像要素を document.getElementsByTagName() メソッドで全て取得します。
もちろん querySelectorAll() メソッドでも良いです。(動的に変更が適応されなくても良い為)

全ての画像ではなく特定の class を持った要素だけにしたい、<MAIN> 要素下の画像だけに適応したい、といった場合にはそれに応じたセレクタを定義して下さい。
複雑な条件の場合は、CSS と同じ指定になる querySelectorAll() の方が簡単かもしれませんね。

あとは、取得順に配列へ URL と要素セレクタを格納しておきます。
URL が一時退避できたので、画像を透明 GIF に置き換えてしまいます。これにより元画像は読み込まれません。

遅延読込処理をイベントハンドラに登録する

準備ができたので、イベントハンドラに遅延読込の処理を登録します。

// <IMG>要素以外の読み込みが終わったら遅延読込を始める window.addEventListener("load", function(){ // 遅延読込を呼び出す // data変数には読み込んでいない画像の配列が戻る data = lazyLoadRun(data, 750); // スクロールイベントを追加する // スクロールすると順次画像が読み込まれる let timerID = null; window.addEventListener("scroll", function(){ if(!timerID){ timerID = setTimeout(function(){ timerID = null; data = lazyLoadRun(data, 500); if(data.length < 1){ timerID = true; } // コンソールに残り画像数を表示させる console.log(data.length); }, 200); } }, false); }, false);

まずはファーストビュー、つまりスクロールしていない領域の画像を読み込みます。
実際に読み込む処理は後述しますので、ここでは関数を "data = lazyLoadRun(data, 750);" と呼ぶだけです。

ここで、lazyLoadRun() 関数に2つの引数を渡しています。
第一引数は先ほど用意した画像の URL とセレクタの配列です。
第二引数には画面外の画像をどこまで読み込んでおくかを px 単位で指定します。

ファーストビューの画面に表示される画像までとすると、スクロールした時に遅れが生じるので予め少しだけ画面外の画像も読み込むようにします。
サンプルでは 750px に設定していますが、画像の容量やスクロールされる速さ (文章量やデザインなど) によるので適宜調整して下さい。

続けて、スクロールイベントも登録します。
スクロールされるたびに lazyLoadRun() 関数を呼べばよいのですが、スクロールイベントはもの凄い頻度で呼ばれるので 、少し間引きしておきます。
(イベント発生タイミングはブラウザや CPU などに依ります)

let timerID = null; window.addEventListener("scroll", function(){ if(!timerID){ timerID = setTimeout(function(){ timerID = null; /********************/ // スクロールイベントで実行したい処理など /********************/ }, 200); } }, false);

これには setTimeout() 関数を使います。
setTimeout() 関数はセットされた時間 [ms] 経過後に指定の処理を実行しますが、セットするとタイマー ID というものを返します。
そこで、処理待ちをしているのかどうかはこの ID を確認すれば良いことになります。

つまり、タイマー ID がセットされていればそのタイミングでのスクロールイベントは無視してスキップさせてしまおうというわけです。
そして、処理が実行される時にタイマー ID を格納した変数を空 (ここでは null) にすれば良いのです。

サンプルでは 200ms の待ち時間を設定しています。
頻繁にスクロールされない場合は、もっと長くしても良いです。ご自分のサイトに適応して調整して下さい。

スクロールイベントで行う処理はファーストビューと同じく、lazyLoadRun() 関数を呼ぶだけです。
また、画像を全て表示終えたらイベントが呼ばれないように timerID 変数を true にしておきます。

data = lazyLoadRun(data, 500); if(data.length < 1){ timerID = true; }

ここでも、画面外の画像をどこまで読み込むか個別で設定しているので適宜調整してみて下さい。

画像を読み込む関数を定義

遅延読込のメイン処理関数を定義します。
メインと云っても画像の位置が表示領域内に入ったら src 属性を書き換えるだけです。

// 画像の読込処理関数 const lazyLoadRun = function($data, $pre){ const returnArr = new Array(); const windowHeight = document.documentElement.clientHeight; for(let j = 0, m = $data.length; j < m; j++){ if($data[j].selector.getBoundingClientRect().top < windowHeight + $pre){ $data[j].selector.src = $data[j].URL; } else{ returnArr.push($data[j]); } } return returnArr; };

画面内にあることを確認したいので、はじめに表示領域の高さを求めます。
基本的には、document.documentElement.clientHeight で IE8 以前の古いブラウザ含めてスクロールバーを含まない表示領域の高さが取得できます。

この高さが正常に取得出来ていない時は <!DOCTYPE HTML> が HTML 文書の最初で宣言されているか確認して下さい。
これを指定しないと互換モードという CSS 普及以前の化石ブラウザとの互換性の為にあるレンダリングモードとなり、正しい値が取得できなくなります。
詳しくは、以下ページをご確認下さい。
・MDN - DOCTYPR
・HTML クリックリファレンス - HTML の基本 - DOCTYPE スイッチ

画面の高さが分かったので、画像の相対位置 (表示されている位置) を getBoundingClientRect() で取得して範囲内であれば src 属性をセットして、範囲外なら未読込用の配列に追加します。
全て確認できたら未読込画像の配列を返して終了します。

DOMが構築されたら以上の処理を実行させる

最後にここまでの処理を DOM ツリーが構築されてから実行されるようにイベントリスナに登録します。
<HEAD> 要素内で Javascript を実行している場合は即時実行してしまうと DOM がなく、画像要素が取得できずうまく実行されません。

// DOM構築が完了したらLazyLoadの準備を行う // <BODY>の一番最後にこのコードを置く場合は即時関数にして以下を削除してもOK window.addEventListener("DOMContentLoaded", lazyLoadSet, false);

コメントアウトして書いていますが、これらのコードを <BODY> 要素の最後に記述すれば上記コードを削除して即時関数かベタ書きで実行可能です。

ショートコード化したJavascriptコード

最後にショートコード化した Javascript コードを紹介します。
DOMContentLoaded イベントを削除するために <BODY> 要素最後に読み込んで下さい。

※表示上、改行していますが全て削除して下さい。

/* ショートコード化したコード (542B) */ /* BODYの一番最後に読み込んで下さい */ !function(b,w){for(var d=[],i=b.querySelectorAll("img"),n=i.length,t;n--;){d.push({u:i[n].src,s:i[n]}); i[n].src="data:image/gif;base64,R0lGODlhAQABAGAAACH5BAEKAP8ALAAAAAABAAEAAAgEAP8FBAA7";} w.addEventListener("load",function(){d=r(d,750);w.addEventListener("scroll",function(){if(!t){ t=setTimeout(function(){t=d.length<1;d=r(d,500);},200);}},!1);},!1);function r($,_){for(var a=[], h=b.documentElement.clientHeight,j=$.length;j--;){if($[j].s.getBoundingClientRect().top<h+_){ $[j].s.src=$[j].u;}else{a.push($[j]);}}return a;}}(document,window);

即時関数の引数に与えている window は this にしたり省略しても動きます。
が、さすがにそこまでやると後から読めなくなる (分かっていれば読めなくもないですが) のでそこまではやりません。
(基本的には window.*** の場合、"window" は省略可能で *** だけで動きます)

即時関数でグローバルの名前空間を汚さないようにしていますが、他のコードとの競合がないのであれば以下のように即時関数を外すこともできます。

/* ショートコード化したコード (531B) */ /* BODYの一番最後に読み込んで下さい */ for(var d=[],i=document.querySelectorAll("img"),n=i.length,t;n--;){d.push({u:i[n].src,s:i[n]}); i[n].src="data:image/gif;base64,R0lGODlhAQABAGAAACH5BAEKAP8ALAAAAAABAAEAAAgEAP8FBAA7";} window.addEventListener("load",function(){d=r(d,750);window.addEventListener("scroll",function(){if(!t){ t=setTimeout(function(){t=d.length<1;d=r(d,500);},200);}},!1);},!1);function r($,_){for(var a=[] ,h=document.documentElement.clientHeight,j=$.length;j--;){if($[j].s.getBoundingClientRect().top<h+_){ $[j].s.src=$[j].u;}else{a.push($[j]);}}return a;}

ショートコードは分かり難いのでかなりシビアに読込速度を気にする方以外は普通にコメントやインデントされたコードが良いでしょう。
メンテナンスも間違える元ですし、gzip 圧縮で転送していれば大きな差にはなりません。

ちなみにですが、このコードは厳密モード (strict mode) で動かすことができます。
小さな Warning を無視しても良い場合は変数宣言などを省略してもっと軽量化することもできます。
Javascript は変数宣言のない変数に代入しても実行できてしまう & "}" の直前の行末セミコロンなどを省略できる、などを利用します

まとめ

以上、超軽量な Javascript コードで遅延読込 (Lazy Load) させるコードの紹介と解説でした。
とても簡単にそして軽量なコードで実現できますね。ダミー画像込みで 542B なら導入検討の余地は大いにありますね。

また、Javascript が無効な場合には普通に画像が表示されるのも良いところだと自画自賛しています。
検索エンジンのクローラから遅延読込のスクリプトファイルがアクセスされないように .htaccess などに設定すればクロールへの影響もありません。
(Googlebot などは一部 Javascript を解釈して動的要素などを検出しているらしいので)

最後にスクリプト使用上で一点だけ注意ですが、ここでは他の処理で <IMG> 要素の src 属性を使用しない場合を想定していますので、そのような場合はその他の処理が全て終了してから遅延読込の処理を実行するようにして下さい。
先にこのスクリプトが実行されると全ての画像の src 属性が透明 GIF になってしまいます。。。
ご注意下さい。