width="400" height="200" と書いた img の実寸が400×300だった。それだけで、読み込みが終わった瞬間に100pxぶんレイアウトがずれます。
画像は重く、しかもページの上部に置かれがちです。仕事でLighthouseを見ながら改善したときに溜まった知見を、ここでまとめます。コード例はTailwind CSSですが、指定しているCSSプロパティを読み替えれば、ほかの環境でも使えます。
レイアウトシフトを防ぐ
画像はネットワークから取得するため、テキストより遅れて表示されることがあります。表示領域を事前に確保していないと、読み込み後に画像の高さだけ後続コンテンツが押し下げられます。クリックしようとしたボタンが突然ずれる体験は、大きなストレスになります。
この予期しないずれがレイアウトシフトです。画像の領域を最初から確保してCumulative Layout Shift(CLS)を抑えることは、Core Web Vitalsだけでなく、実際の使いやすさの改善にもつながります。
サイズがわかるコンテンツ
画像の寸法がわかる場合は、img要素のwidth属性とheight属性へ正しい値を指定します。レスポンシブ表示では、CSS側のwidthとheightも整えます。するとブラウザは、次のように領域を確保します。
- width 属性 と height 属性をもとにアスペクト比を計算する
- アスペクト比とスタイルで定義された width、height を利用して必要な領域を確保する
- ※ プロパティがない場合は属性で指定された値を確保する
- 画像のダウンロードが終わったら領域に表示する
次のコードでは、画像の幅が400px、高さが300pxなので、表示領域の高さは「表示幅 × 3/4」になります。CSSの width: 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分のずれが発生します。画像の寸法がわかっているなら、属性には必ず正しい値を指定します。
サイズがわからないコンテンツ
ユーザーが投稿した画像やAPIが返す画像など、事前に寸法がわからない場合もあります。画像だけでなく、iframe や外部スクリプトによる埋め込みも、描画できるようになるまで高さがわからないことがあります。
このような場合は、CSSの 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" />
aspect-ratio は img 要素以外にも適用できるため、iframe などでも同じ方法で領域を確保できます。
LCP を最小にする
Largest Contentful Paint(LCP)は、ビューポート内の主要なコンテンツが表示されるまでの時間を示す指標です。対象が画像なら、その画像を遅延読み込みせず、HTMLから早い段階で発見できるようにすることで、読み込み開始の遅れを減らせます。
画像を優先的に読み込む
LCP画像には fetchpriority="high" を指定すると、ブラウザへ優先度のヒントを渡せます。これは命令ではなくヒントです。すべての画像を優先すると互いに競合するため、対象は重要な画像に絞ります。
CSSの背景画像など、HTMLから直接発見できない画像には、プリロードで早期に発見させる方法もあります。
<link rel="preload" href="image.webp" as="image" />
Next.js 16以降では、Imageコンポーネントの preload propでプリロードを指定できます。旧バージョンで使われていた priority propは非推奨になっています。
LCP候補の画像に loading="lazy" を指定してはいけません。遅延読み込みによって、画像の取得開始が遅れるためです。
LCP 指標の対象要素を HTML ソースから検出できるようにする
CSSやJavaScriptを経由して画像を読み込むと、それらのファイルを取得・解析するまで画像のURLを発見できません。LCP画像は可能な限り <img src="..."> や srcset としてHTMLに含めます。CSSの背景画像などHTMLに直接書けない場合は、プリロードを検討します。
画像サイズを小さくする
画像が軽ければ、ダウンロードは早く終わります。単純な話ですが、効果は大きいです。picture 要素や srcset、sizes 属性を使えば、表示条件に合うサイズや形式の画像を配信できます。
画像の最適化
画像形式は一律に決めず、内容と配信環境に合わせてAVIFやWebP、JPEG、PNGなどを選びます。Squooshなどで見た目を確認しながら圧縮し、必要以上に大きな寸法の画像を配信しないことも重要です。
Next.jsのImageコンポーネントや画像CDNを使えば、ブラウザと表示サイズに合う画像を配信できます。形式の名前だけで決めず、転送量と画質を測った結果で選びます。
画像最適化は、属性を一つ足して終わる作業ではありません。どの画像が主役なのかを決め、適切な寸法と形式で配信し、読み込みの順番を整えるところまでが設計です。