WordPress:srcset, sizes, lazyを極める

WordPress:srcset, sizes, lazyを極める

これまでは、記事に画像を挿入したり the_post_thumbnail() を使って画像を表示する場合でも、さほど深く考えずに WordPress に任せてきたのですが、あらためてレスポンシブ Web デザインでの画像表示の最適解を探ってみようと思います。

01レスポンシブ Web デザインのサンプル

具体的に考えていくために次のサイトを例に取ります。

このサイトのトップページのファーストビューは縦横ともに幅いっぱいにメインビジュアルを表示し、スクロールしますと映画の一覧表示となります。下図は少しスクロールした状態の画像ですので上部の中途半端な画像はファーストビューのメイン画像の一部です。まずは一覧表示の画像について調べますのでこの状態をキャプチャしました。

このサイトは、レスポンシブになっていますが、メディアクエリにモバイル、PC という考えは取っておらず、投稿ページの本文(上図の映画一覧も同じ…)を表示するメインブロックの横幅を最大 650px として、その幅を確保できないデバイスではメインブロックをデバイスの横幅 92% で表示し、それ以上の横幅を持つデバイスでは、サイドバー用の 300px + マージン 50px をプラスした合計 1000px を確保できるまでは左右の余白を広げ、1000px を確保できるデバイスであれば、投稿ページではサイドバーを表示し、トップページではメインブロックを 1000px まで広げ(上図…)、それ以上の横幅を持つデバイスでは メインブロックを 1000px 固定で左右の余白を広げるというメディアクエリになっています。

そうしますと、トップページの各映画のサムネイルは、メインブロックに 650px を確保できるまではデバイス幅の 92% で表示され、650px を確保できるデバイスでは 300px の 2 カラム表示となり、1000px を確保できるデバイスでは 3 カラム表示になるということになります。

このサイトを例にして話を進めます。

02WordPress の srcset, sizes, lezy デフォルト設定

上の映画一覧の画像を出力しているのは次のコードです。WordPress のバージョンは 6.8 です。

<figure class="p-cardL__thumbnail">
<a href="<?php the_permalink(); ?>"><?php has_post_thumbnail() ? the_post_thumbnail() : the_dummy_image(); ?></a>
</figure>

the_dummy_image() はアイキャッチ画像が指定されていない場合にダミー画像を出力する独自関数で今回の件には関係はありません。詳細は「WordPress:ダミー画像をアイキャッチ画像と同様に出力する」にあります。

上のコードが吐き出す HTMLソースは次のようになります。説明に不必要なものは削除してあります。画像はすべて 1920px × 1080px で作成されています。

<img width="1920" height="1080" 
    src="a-different-man.jpg" class="" alt="" 
    decoding="async" 
    fetchpriority="high" 
srcset="a-different-man.jpg 1920w, 
        a-different-man-300x169.jpg 300w, 
        a-different-man-1024x576.jpg 1024w, 
        a-different-man-768x432.jpg 768w" 
sizes="(max-width: 1920px) 100vw, 1920px" />

また、4つ目の画像からは次のように loading=”lazy” と sizes 属性に auto が追加されます。

<img width="1920" height="1080" 
    src="natsu-no-suna-no-ue.jpg" class="" alt="" 
    decoding="async" 
    loading="lazy" 
srcset="natsu-no-suna-no-ue.jpg 1920w, 
        natsu-no-suna-no-ue-300x169.jpg 300w, 
        natsu-no-suna-no-ue-1024x576.jpg 1024w, 
        natsu-no-suna-no-ue-768x432.jpg 768w" 
sizes="auto, (max-width: 1920px) 100vw, 1920px" />

この状態でページ読込時に映画一覧画像がどう読み込まれているかを Chrome のデベロッパーツールで見てみます。この例の画像は全部で 9個あります。

5個の画像だけしか読み込まれていません。最初の 3個がフルサイズで読み込まれ、4つ目からの 2個が 300px のサイズで読み込まれています。残りの 4個はまだ読み込まれていません。

なぜこうなるかをざっと確認しますと、まずブラウザは html を上から順番に読み、img タグがありますとその画像を非同期(だったと思う…)で読み込み始めまず。ただ、この段階ではレンダリングが完了していませんので表示範囲がわからず、ブラウザは指定された width, height のサイズで読み込みます。それが 3つ目までフルサイズで読み込まれる理由です。

html の解析はさらに進み img タグに loading=”lazy” がありますとブラウザは画像の読み込みをスキップします。そして解析が終わりますとビューポートのサイズもわかりますので遅延読み込みのしきい値を計算してスキップした画像を auto 指定にもとづき最適サイズで読み込みます。それが 4つ目と 5つ目の画像が 300px で読み込まれた理由です。その後、スクロールが発生すればやはりしきい値の範囲内でスキップした画像を読み込んでいきます。

WordPress デフォルトの loading=”lazy” と sizes=”auto” が有効に働いているということです。ただし、この sizes=”auto” は現在のところ firefox と safari では未対応のようです。

詳しくは Can I use か上のリンク先でご確認ください。

03lazy, sizes をいじるフィルターフック

ということで、まず loading=”lazy” と sizes=”auto” を外して sizes がどう働くのかを見てみましょう。

WordPress においては、この loading=”lazy” と sizes=”auto” は連動する属性であり、loading=”lazy” がある場合にのみ sizes に auto が追加されます。ですので、loading=”lazy” の属性を外せば sizes に auto は追加されなくなります。次のフィルターフックで外れます。

add_filter('wp_lazy_loading_enabled', '__return_false');

このフィルターフックを当てますと、画像の遅延読み込みが解除されて sizes は “(max-width: 1920px) 100vw, 1920px” の値だけになります。つまり、デバイス幅 1920px まではデバイス幅いっぱいに表示し、それを越えた場合は 1920px で表示するという設定になります。

なお、上のフィルターフックとは別に sizes の auto だけを解除するフィルターフックもあります。

add_filter( 'wp_img_tag_add_auto_sizes', '__return_false' );

loading=”lazy” を外し、必然的に sizes=”auto” も外れた結果です。これを見てみますとページ読込時に 9個すべての画像のフルサイズデータを読み込んでいます。

次に sizes の設定を変えてみます。sizes を変更するフィルターフックは wp-includes/media.php の 1581行目の関数 wp_calculate_image_sizes() の中にあります。

function imz_calculate_image_sizes () {
    if ( is_front_page() )  {
        return '(max-width: 706px) 92vw, 300px';
    }
}
add_filter('wp_calculate_image_sizes', 'imz_calculate_image_sizes');

メディアクエリの 706px というのは 650px の表示幅を確保でき、かつ左右に 4% ずつのマージンが取れる、つまりメインブロックを2カラムにできる 707px を越えない値ということで、その値までは 92vw で表示せよという意味になります。706px を越えますと2カラム、1086px を越えますと 3カラムになり、どちらも画像の表示幅は 300px で固定されるということです。

結果はすべての画像で srcset の中から 300px の画像が選ばれて読み込まれています。loading=”lazy” がありませんのでページ読込時にすべての画像が読み込まれます。

同じ設定でブラウザの表示幅を 706px にしてみます。

ブラウザの表示幅が 706*92%=649.5px となりますので sizes のメディアクエリによって srcset の中から最適サイズの 768px が読み込まれています。sizes の指定通りになっているということになります。

これで sizes 属性の使い方は理解できました。

04loading=”lazy” と sizes=”auto” の最適解

次に loading=”lazy” と sizes=”auto” がどう働くかを具体的に見ていきます。

この例では画像の容量をかなり落としてありますので大きな問題はないようにも思いますが、画像 1個が 200KB ともなれば、他の一覧ページでは 1ページに 20個表示しますのでそれだけで 4MBになってしまいます。

そこで遅延読み込み loading=”lazy” 属性です。この属性はページ読込時にビューポートの外にある画像(iframe にも設定可…)に遅延読み込みを指示します。現在の主要ブラウザはすべて対応しています。

ただ、その画像がビューポート内に入るかどうかは HTML を送り出す PHP からはわかりませんので WordPress では 4 つめの画像から loading=”lazy” を挿入する仕様になっています。

そのコードは wp-includes/media.php の 6197行目の関数 wp_omit_loading_attr_threshold() にあります。

function wp_omit_loading_attr_threshold( $force = false ) {
	static $omit_threshold;

	// This function may be called multiple times. Run the filter only once per page load.
	if ( ! isset( $omit_threshold ) || $force ) {
		/**
		 * Filters the threshold for how many of the first content media elements to not lazy-load.
		 *
		 * For these first content media elements, the `loading` attribute will be omitted. By default, this is the case
		 * for only the very first content media element.
		 *
		 * @since 5.9.0
		 * @since 6.3.0 The default threshold was changed from 1 to 3.
		 *
		 * @param int $omit_threshold The number of media elements where the `loading` attribute will not be added. Default 3.
		 */
		$omit_threshold = apply_filters( 'wp_omit_loading_attr_threshold', 3 );
	}

	return $omit_threshold;
}

ですので、遅延読み込みを何個目の画像からにするかは次のフィルターフックで変更できます。

function imz_lazy_loading_threshold() {
    return 1; // 2つめの画像から loading=”lazy” を挿入
}
add_filter( 'wp_omit_loading_attr_threshold', 'imz_lazy_loading_threshold' );

このフィルターフックで 0 を返してみます。すべての画像に loading=”lazy” がつきます。sizes の値もデフォルトの sizes=”auto, (max-width: 1920px) 100vw, 1920px” に戻しておきます。

デフォルトですと 3つの画像はフルサイズを読み込んでいたものが 300px になっています。遅延読み込みが働いているのは間違いないでですね。それに sizes=”auto” も有効だということになります。

ただ、5個の画像が読み込まれるのは変わりません。おそらくブラウザのしきい値の問題だと思いますが、試しに画像の位置を下げてみてもほとんど変わりませんでしたので今のところよくわかりません。ちなみに Chrome のしきい値は 1250px との記事があります。

05まとめと W3C Validator のエラー

ということで、この例のケースだけという意味では次のフィルターフックを functions.php に書いておくのが最適解ということになります。他のページのことを考慮していませんのでこのままでは使えませんが考え方の一例です。

function imz_calculate_image_sizes () {
    if ( is_front_page() )  {
        return '(max-width: 706px) 92vw, 300px';
    }
}
add_filter('wp_calculate_image_sizes', 'imz_calculate_image_sizes');

function imz_lazy_loading_threshold() {
    return 0; // 1つめの画像から loading=”lazy” を挿入
}
add_filter( 'wp_omit_loading_attr_threshold', 'imz_lazy_loading_threshold' );

ところで、sizes=”auto, (max-width: 1920px) 100vw, 1920px” の設定を W3C Validator でチェックしますと次のエラーが出ます。

sizes の値に autoと数値(値は何であっても…)を入れていることに対するエラーだと思います。

これについては auto に対応していないブラウザは数値にフォールバックしますので問題ないということです。次のリンク先をご覧ください。

ということで今回はここまでです。

実際のスマートフォンではどうなるかや decoding=“async” については次回です。