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が不要になったら上でやった部分を消すだけなので原状復帰も簡単。

関連記事:

WordPress用無料テーマ Bonyo/凡庸 v1.1フル機能版

ブログ
©いらすとや.

「がとらぼ」の中の人が作成しているWordPress用の無料テーマBonyo/凡庸 (汎用はんようじゃないよ)のv1.1 フル機能版をリリースしました。

フル機能版だけど完全無料で制限は無し。有料版があるわけではないのでアップグレードを促すメッセージがウザいとかも無し。難しいことをしているわけではないのでBonyo/凡庸を使うということではなくても使えそうな部分があったらテキトーにコピって利用して貰って全然OKです。

Bonyoの詳細とダウンロードは↓から

WordPress用テーマ Bonyo (凡庸)


v1.1の新機能
  • WordPressの管理パネルでテーマ中の各種色カスタマイズ
  • カスタマイズ内容を含めてCSSを最小化して静的出力
  • AMPプラグイン用のテーマ
  • 通常ページとAMPページでCSSを共通化
  • サイドバーをAMP用に変換して静的出力

v1.0までで実装済みの機能と特徴
  • CSSを自動的に最適化してそれを使う機能
  • 画像の遅延読み込み
  • サイドバーの動的/静的化を選択可
  • Javascriptフリーな多階層メニューとハンバーガーメニュー
  • レスポンシブ2カラム→1カラム

v1.1では色関係のカスタマイズとAMP周りの追加がメイン。

色のカスタマイズはv1.0まではCSS変数による「なんちゃって」カスタマイズだったが、WordPressの管理パネルからのカスタマイズになったのでようやく普通のWordPressのテーマ的なカスタマイズができるように見えるかも。しかし、普通のWordPressのテーマカスタマイズ(の反映方法)は好きではないので変更内容をスタイルシートに含めて最小化してファイル出力するようにした。(同時にAMP用にも)

AMPはベース部分はWordPressのオフィシャルのAMPプラグインをClassicモードで使うことを前提としている。PairedモードやNativeモードの変換は不具合が多くて好きではないので。
Classicモードは基本的にはAMP専用のテーマにスタイルシートとAMP化した本文を嵌めて出力するものだが、Bonyo/凡庸ではほぼ通常テーマと同じ見た目のAMP用テーマを同梱し、さらにサイドバー部分をAMP変換して静的出力する機能を用意し、これらを合わせてAMPでもほぼ通常のページと同じ見た目になるようにした。静的サイドバーなのでAMP化ページの出力を遅くすることもない。ただし、AMPでは勝手Javascriptが許されてないのでJavascriptを使ったウィジェットを含むと通常ページと完全に同じ見た目にはならないかもしれない。

WordPress用無料テーマ凡庸 1
WordPressの管理パネルのテーマカスタマイズに「色」と「オプション」のセクションを設けた。テーマの基本的なカスタマイズはここから。

WordPress用無料テーマ凡庸 2
「色」のカスタマイズセクション。WordPressの他のテーマと同様にカスタマイズした内容は右側のプレビュー画面に反映される。 Bonyo/凡庸では色だけでなく色の透明度も指定できるので透明度を活かしたデザインにしていただければと。上の画像では本文の色を透明度の高い赤(ピンクっぽく見える)に変更したところ。

WordPress用無料テーマ凡庸 3
「オプション」のカスタマイズセクション。Bonyo/凡庸の機能を有効・無効を選択できる。
都合によりページの背景画像の指定項目もここに置いた。

WordPress用無料テーマ凡庸 4
AMPプラグインはWordPressのオフィシャルのもの(作者がAutomattic)を使用することを前提としている。AMP for WPではないので注意。
インストールしたら設定画面で「Classic」モードを選択して「設定を保存」を押す。テーマがBonyo/凡庸でWordPressの標準ウィジェットを使う限りはAMPエラーは発生しない筈。GoogleのAMPテストでAMPエラーになるようであれば改良の余地ありなのでお知らせ下さい。

v1.2以降では「最近の記事」一覧をハードコーティングではなくamp-listとWordPress標準のREST APIを利用して動的にページに嵌める機能を実装したい。これによりAMPキャッシュであっても常に最新の「最近記事」一覧が表示できるようになる。「がとらぼ」のAMPページでは実装済み。
あとはウィジェットに広告コードを書いている場合のAMP用への切り替えを実装したいと思ってるけど、こちらはまだ「できたら良いなぁ」レベルなので具体的にどうすれば実現できるのか足りないアタマで悩み中。

関連記事:
Up