お仕事で Lighthouse を確認しながら改善を進めていたタイミングで画像に関する知見が少し溜まったのでそれをまとめます。 tailwind css のクラス名が途中出てきますがなんとなく読み取っていただけたら嬉しいです…。
レイアウトシフトを防ぐ
画像の表示はダウンロードが発生するためテキストより後に表示されることが普通です。 何も指定しないと初期段階で画像の表示領域は 0px であり、画像のロード後に必要なだけの高さを確保します。そのためユーザがボタンをクリックしようとした直前に、画像の表示が完了すると画像の分だけボタンの位置がずれてしまいます。これはユーザにとって大きなフラストレーションとなります。そしてこのレイアウトのズレがレイアウトシフトと言われています。レイアウトシフトの発生を極力抑制することで Core Web Vitals の改善、ひいてはユーザ体験の改善に繋がります。 つまり画像の表示領域を必要な分だけ始めから確保しておけばユーザ体験が損なわれないようになります。
サイズがわかるコンテンツ
どの画像を利用するかわかる場合、つまり画像の横幅と高さがわかる場合は img 要素の width 属性と height 属性を指定します。 画像をレスポンシブに表示したい場合はスタイルで width と height を定義します。 その上で画像をレスポンシブに表示したい場合はスタイルとして width と height を指定します。この時のブラウザの挙動は下記のようになります。
- width 属性 と height 属性をもとにアスペクト比を計算する
- アスペクト比とスタイルで定義された width、height を利用して必要な領域を確保する
- ※ プロパティがない場合は属性で指定された値を確保する
- 画像のダウンロードが終わったら領域に表示する
例えば下記のようなコードであれば、写真の横幅が 400px で高さが 300px なので、画像の表示領域は「横幅 * 3/4」になります。 横幅は親要素の 100%なので実数値ではありませんが、高さと異なりブラウザ幅が描写タイミングでは分かるのでそれを辿っていけば高さもわかりますね。
<img
src="https://picsum.photos/400/300"
width="400"
height="300"
class="w-full h-auto"
/>
次に画像の実際のサイズと width height 属性が異なっていた場合です。例えば下記のようなコードでかつ親要素の高さが十分にある場合にはレイアウトシフトが起きます。
<img
src="https://picsum.photos/400/300"
width="400"
height="200"
class="w-100 h-auto"
/>
ブラウザの動きから推測するにこのようなことが発生してそうです。
- width 属性 と height 属性をもとにアスペクト比を計算する
- アスペクト比とスタイルで定義された width、height を利用して必要な領域を確保する
- クラスでの width の指定が 400px なので、
200/400 * 400px
で算出される 200px 高さを確保する
- クラスでの width の指定が 400px なので、
- 画像のダウンロードが終わり実寸が確定したらアスペクト比を再計算する
- 画像の実際のアスペクト比とスタイルで定義された width、height を利用して必要な領域を確保する
- クラスでの width の指定と画像の正しいサイズをもとに
300/400 * 400px
で算出される 300px 高さを確保する
- クラスでの width の指定と画像の正しいサイズをもとに
なので 100px 分のズレが起こります。0px からのズレよりはマシですがわかっているのであれば正確な値を入れてあげましょう。
サイズがわからないコンテンツ
事前に高さと横幅がわからない場合も多々あります。例えばユーザの投稿した画像や API が返す画像などの場合です。また画像だけでなく埋め込みコンテンツ、iframe や外部スクリプトによる広告なども同様に要素を描写できるようになるまで高さを把握できません。 aspect-ratio を指定することでボックスのアスペクト比を決めることで必要な領域を事前に確定することができます。
下記の場合は aspect-ratio: 16 / 9
を指定するため、横幅が 400px なので高さは 225px になります。画像のサイズは分からないとしても必要な領域が確保できるのでレイアウトのズレがなくなります。ただしこれだけだと画像が引き伸ばされてしまうので、 object-fit
も同時に指定してあげます。画像のアスペクト比を保ちながら表示領域を覆うようにしたいのであれば cover
を、アスペクト比を保ちながらボックスに収まるように表示したいのであれば contain
を指定するとうまく働きます。
<img
src="https://picsum.photos/400/300"
class="w-100 h-auto aspect-video object-cover"
/>
apect-ratio は img 要素だけでなく適用できるので、iframe などに関しても同様のテクニックで必要なだけスペースを確保することができます。
LCP を最小にする
Largest Contentful Paint (LCP) はビューポート内で表示される一番大きな画像かテキストブロックの表示にどれだけ時間がかかっているかになります。 多くの場合は画像になるのですが、LCP にあたる画像だけは遅延読み込みをせずに優先的に読み込ませること、HTML ソースから画像を検出できるようにすることで LCP の値は大幅に改善されます。
画像を優先的に読み込む
Chronium ベースのブラウザでは fetchpriority="high"
を img 要素に付与することで画像を優先的にダウンロードするようにしてくれます。それ以外のブラウザではまだ実装が進んでいないこともあるので head タグの中に <link rel="preload" href="image.webp" as="image">
のようなコードを入れることで優先的にリソースの取得をするようにしましょう。
Next.js であれば Image コンポーネントにある priority prop を指定することで簡単に対応することができます。
また当然ですが LCP 指標の対象となる画像に関しては loading=lazy
を付与するべきではありませんし、重要でないリソース(分析用のスクリプトなど…)は body の下の方に記述することで読み込みの優先度を下げましょう。
LCP 指標の対象要素を HTML ソースから検出できるようにする
<img src="..." />
、<link href="" />
といった標準の HTML 属性ではなく、CSS や JavaScript 経由で画像のソースを読み込む場合には CSS や JavaScript がダウンロードされて解析と処理が終わってから画像のロードが始まるため純粋な画像のロード以外にも多くの時間を費やしてしまいます。これは LCP 指標が悪化する原因となってしまいます。
なので LCP 要素が画像である場合は常に HTML ソースから画像の URL が検出できるようにすることで LCP 指標を改善することができます。
画像サイズを小さくする
当然ですが画像サイズが小さくなればダウンロードにかかる時間が短くなるため各種指標が改善されます。 合わせて picture タグや srcset 属性を活用することでデバイスごとに最適なサイズの画像を簡単に配信できます。
画像の最適化
画像配信の際には WebP 形式を利用しましょう。特に png 形式を変換した時のサイズ現象は大きなものになります。 また画像に関して squoosh などを利用して可能な限り圧縮をしてあげることでファイルサイズを抑えることができます。 Next.js では Image コンポーネントを利用することで元の画像が jpg や png だとしても配信時に webp に動的に切り替えてくれます。