WordPressで画像遅延読み込みを実装する

WordPress 5.5でLazy-loadが実装されたのでこの記事の内容は不要です。

そろそろブラウザに画像遅延読み込みが標準実装されそうな気配ではあるけれど、まだ無条件で使える状態ではなさそう。Chromeブラウザであればバージョンによって対応しているか不明だけど chrome://flags/#enable-lazy-image-loading で設定できる。2019年3月13日現在では少なくともChrome 73.0Betaでは機能の切り替え項目が表示された。初期値は遅延読み込み無効みたい。
現状では閲覧者が積極的に遅延読み込みを有効にしないと機能しないし、Chromeでは遅延読み込みが使えても他のブラウザでは使えないのでは意味がない。
そこで、ウェブサイト側で画像遅延のJavascriptのEcho.jsを使うことにした。WordPress用の無料テーマBonyo/凡庸にも採用した。Echo.jsはとても小さくて簡単なのが採用理由。
その実装メモ。

Echo.jsによる画像の遅延読み込みを使うために行うべき手順は3つ。

1番め
echo.min.jsの1ファイルを貰ってきてそれをウェブサイトに置く。または内容をコピーしてHTMLヘッダ内に書く。

2番め
Echo.jsがページ内で読まれるようにする。


<html>
  <head>
    中略
    <script src="//場所/echo.min.js"></script>
  </head>
  <body>

    ページの本文など

    <script>
      echo.init();
    </script>  
  </body>
</html>

head内の4行目でEcho.js本体を読み込んでbodyの最後あたりの11行目で初期化する。こんだけ。Echo.jsは非常に小さいので別ファイルにせずにhead内に埋め込む方がオススメ。

3番め

画像のHTMLタグである<img hoge />を書き換える。

一般的にはimgタグはこんなふうに書かれる。

<img class="なんか" src="画像の場所/画像ファイル名" alt="画像の説明" />

Echo.js用には次のように書き換える。
<img class="なんか" src="ダミーロード" data-echo="画像の場所/画像ファイル名" alt="画像の説明" />

変更前はsrcに表示する画像を指定していたが、そこがダミーロードになる。ダミーロードは1ピクセル画像でもグルグルGIFでも何でもよい。少なくともサイズの小さい画像。さらにはページ内に複数の画像を貼るのであればダミーロードは同じ画像を使用するのが望ましい。
本来表示したい画像はdata-echoで指定する。

こんだけ。

以上で、ページを表示すると、先ずは画像が表示されるべき部分にダミーロードが表示される。ダミーロードが非常に小さい画像であればページ表示が画像表示のために遅くなることはない筈。というか、ファーストビューの範囲に画像が無ければほぼ画像無し同様の速度が得られる。ファーストビュー(スクロールせずに表示される部分)の範囲内に画像を表示する必要があればEcho.jsによってdata-echoに指定された画像を読み込んで、それが表示される。
その後、スクロール表示すると表示範囲内に画像を表示しなければならないタイミングでdata-echoで指定された画像を転送して表示する。

問題は検索エンジンのクロールではこの遅延読み込み用のimgタグを正しく認識できなくて画像を取得してくれないこと。つまり上のimgタグはSEO的にはよろしくない。
そこで、クローラー向けにはnoscriptで画像を表示するようにする。
つまりimgタグはこうなる。
<img class="なんか" src="ダミーロード" data-echo="画像の場所/画像ファイル名" alt="画像の説明" />
<noscript><img class="なんか" src="画像の場所/画像ファイル名" alt="画像の説明" /></noscript>
Echo.js用に書き換えたimgタグ + 元のimgタグをnoscriptで囲ったセット。

WordPressに実装

記事数が1つ2つのウェブサイトで、静的HTMLファイルで作っているなら素直に上のようにimgタグを書き換えることになるが、WordPressでたくさんの記事を抱えているなら全ての画像のタグを書き換えるのは大変。ましてや、今後はブラウザが遅延読み込みに対応するかもということでいつか元に戻したいというときに再びすごい手間になる。そこで、記事の作成時は普通にimgタグを書いて、表示のときに遅延読み込みように変換する。それが普通のアプローチかと。

WordPressではこの手のことをするときは基本的に使用しているテーマのfunctions.phpに処理を挿入する。

functions.php (最後あたりにでも追記する)
function 自作の関数($content) {
    関数の処理
    return 書き換えた中身;
}
add_filter('the_content', '自作の関数名');              //記事本文の書き換え
add_filter('post_thumbnail_html', '自作の関数名');      //アイキャッチ画像

アイキャッチ画像のタグ変換は任意。アイキャッチ画像は使わないという方針なら変換不要な筈。自作の関数はこの下で作成する。imgタグの書き換えだけであれば同じ処理なので記事本文用とアイキャッチ画像用のどちらも同じ関数名を指定で良い。

タグ変換用の関数

普通はpreg_replace()を駆使したいだろうし、この文字列の置換でも出来なくはない。ただ、複雑になりがちなのとWordPressでの記事本文の書き方によっては予想外な挙動の元になるのであまりお薦めしない。
手間な部分はあるけどDOMでやる。ただし、こちらもタグのとじ忘れがあると勝手に修正されるなどで意図しない表示になることはある。(ブラウザによる自動修正の方が気が利いていることが多い)

で、DOMでやる場合、英語圏ならあまり考えずに手っ取り早くイケるんだろうけど、日本語などは中途半端に困る。
$contentが記事本文部分であるとしてloadHTML()で迂闊に読み込ませるとISO-8859-1以外は化けると思っていい。かといってloadHTML()の際に mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8')のようにHTMLエンティティに変換するのもイヤ。そこでイケてないけど$contentの前後にHTMLのヘッダとフッタを付ける。ここで文字コードを指定してやると化けない。(後の</body></html>は書かなくても勝手に付くけど)
ネットで探したところ <?xml encoding='UTF-8'> + 操作したいドキュメント でもイケるという情報があった。しかし、試したところ確かに化けはしなかったがHTMLエンティティに変換されていた。

@$dom->loadHTML($buf, LIBXML_HTML_NODEFDTD | LIBXML_NOERROR); という指定にすると<!DOCTYPE html PUBLIC・・・>という行が付かないので除去処理も不要になる。また、XMLとしてのエラー扱いを抑止できるので良いかもしれない。下ではやってないけど。

$buf  = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>';
$buf .= $content;
$buf .= '</body></html>';

上では1行目のmeta内でウザい書き方をしているが、文字化けしない間違いない方法なのよね。
で、DOMドキュメントを作成してloadHTML()で放り込む。

$dom = new DOMDocument();
@$dom->loadHTML($buf);

DOMになったら簡単。(クセはあるけど)
$images = [];   //配列を作る
foreach ($dom->getElementsByTagName('img') as $node) {  
    $images[] = $node;      //imgタグを見つけてノードとして配列に入れる
}

foreach ($images as $node) {

     順にノード(imgタグ)を弄る処理をここに書く

}

ノードを弄る部分
$fallback = $node->cloneNode(true);                //元ノード(imgタグ)の複製ノードを作る

$orgsrc = $node->getAttribute('src');              //元のimgタグのsrc属性の値(URI)を取得する
$node->setAttribute('src', 'ダミーロードのURI');     //src属性にダミーロードのURIを取得指定

$node->setAttribute('data-echo', $orgsrc );        //新属性のdata-echoを作成して元のsrc属性の値(URI)を指定

$noscript = $dom->createElement('noscript', '');   //新要素としてnoscirpt(タグ)を作成する
$node->parentNode->appendChild($noscript);         //触っているimgノードの親にnoscirptを子供ノードとして追加(imgと同列の兄弟)
$noscript->appendChild($fallback);                 //noscriptノードの子供として複製した元ノード(imgタグ)を付ける(noscriptに入れ子

要素(ノード)自体は基本的にappendChild()で触ることになる。「変更」というのも無いっぽい。つまり同列とか親を直接触れない(編集できない)ので親を指定してその子供を作ることで同列の操作になる。ここは個人的には普段触ってないとすぐに忘れてわからなくなってイヤになる。

必要な変換が終わったら$dom->saveHTML()で文字列に戻す。
最後に前後に付けた不要な文字列<!DOCTYPE html><html><head>...</head><body>と</body></html>を削除する。

処理の終わったものを返り値として返す。

処理用関数はまとめるとこんな感じ。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function 自作の関数($content) {

    $loaderuri = get_template_directory_uri() . '/images/loader.gif';  //ダミーロードURI

    $buf  = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>';
    $buf .= $content;
    $buf .= '</body></html>';
    $dom = new DOMDocument();
    @$dom->loadHTML($buf);

    $images = [];
    foreach ($dom->getElementsByTagName('img') as $node) {  
        $images[] = $node;
    }

    foreach ($images as $node) {
        $fallback = $node->cloneNode(true);

        //src
        $orgsrc = $node->getAttribute('src');
        $node->setAttribute('src', $loaderuri);

        //data-src
        $node->setAttribute('data-echo', $orgsrc );

        //noscript
        $noscript = $dom->createElement('noscript', '');
        $node->parentNode->appendChild($noscript);
        $noscript->appendChild($fallback); 
    }

    $buf = preg_replace('/^\<\!DOC.*?\>\n/', '', $dom->saveHTML());
    $buf = preg_replace('/^\<html\>.*?\<body\>/', '', $buf);
    $buf = preg_replace('/\<\/body\>.*?\<\/html\>\n/', '', $buf);

    return $buf;
}
実際は7行目は勝手に補完される部分なので不要。

いつかEcho.jsが不要になったら上でやった部分を消すだけなので原状復帰も簡単。

関連記事: