Next.js 13のImageコンポーネントの簡単な使い方とメリット
はじめに
Next.jsには、HTMLのimgタグを拡張したImageコンポーネント("next/image")というものがあります。このサイトもNext.jsで構築していますが、Imageコンポーネントは使い方やメリットがよく分からず、今までは使っていませんでした。
最近、imgタグのsrcsetやpictureタグ等を使った画像最適化手法を学んだので、ちまちまと異なるサイズの画像を手動で準備していました。
そこで、「そういえばNext.js 13のアップグレードでImageコンポーネント新しくなってたな」と思い出し、ドキュメントを読み返してきました。
すると、「異なるサイズの自動生成」、「デバイスに応じた適切な画像サイズの提供」等、画像最適化を自動で行ってくれると記載されているではありませんか!
実際に試してみると、imgタグのsrcset等を使って最適化するより、かなり手間を減らすことが出来たので、使い方やそのメリットを、記事にして共有したいと思います。
内容
Next.js 13のImageコンポーネント("next/image")が前提です。Next.js 12以前のバージョンとは、後方互換無しのアップグレードがされているのでご注意ください。
Imageコンポーネントのwidth、height、sizes、priority、fill等のプロパティについて、使い方とともに解説します。
configに手を加えることで、細かい調整を行うことも可能ですが、私もまだ試せていないので範囲外となります。
前提知識
imgタグのsrcset
imgタグのsrcset、もしくはpictureとsourceタグを使った画像最適化手法を知っていたほうが、俄然理解しやすいです。こちらに記事にしていますので、良かったら事前に見て行ってください。
【Core Web Vitals】imgタグをレスポンシブにする方法
知らないと、Imageコンポーネントの便利さが分かりにくく、いまいちメリットが感じづらいかと思います。ただし、ご存知でなくても使うこと自体はそこまで難しくないので、お気軽に読み進めてください。
Core Web Vitals
WebのUXの指標として、Core Web Vitalsを知っていたほうが、メリットが感じやすいと思います。こちらは、指標の細かい計算方法までは把握する必要はありません。大まかにどのような指標があるか、把握されていれば大丈夫です。
Imageコンポーネントを使うメリット
パフォーマンス、UX改善
公式だと以下のように強味が説明がされています。私が和訳しています。
- パフォーマンス改善: モダンな画像フォーマットで、常にデバイスに応じたサイズで提供します。
- 安定した表示: Cumulative Layout Shiftを自動で防止します。
- より早いページのロード時間: 画像はビューポートに入った時にのみ読み込まれます。読み込みされるまでのボケ画像のオプション付き。
- 資産の柔軟性: リモート・サーバにある画像でも、オン・デマンドの画像リサイズに対応。
Core Web Vitals対応になる
UXに対するガイダンスとして、Google社はCore Web Vitalsを提示しています。その中で、ページのロード時間や、レイアウト・シフトに関する指標が盛り込まれています。
スマホのようなディスプレイの小さいデバイスでページを開いた時に、不要に大きな画像をダウンロードしてロード時間が長くなってしまったり、画像読込後に直下のコンテンツが画像の高さ分シフトしてしまったりすると、「UXが良くない」とSEO面でも不利に評価されてしまう可能性があります。
Imageコンポーネントはこのような問題に対処してくれます。
imgタグを使うより楽
imgタグのsrcsetでデバイスごとに表示を切り替える方法だと、デバイス別の画像を準備するのがかなり面倒です。Imageコンポーネントは複数サイズの画像を自動で生成し、デバイスに応じて適切なサイズの画像を選んでくれるます。かなり手間が省けるので便利です。
Imageコンポーネントのデメリット
imgタグのsrcsetを使う場合、切り替える画像は自分で好きに選ぶことができます。 しかし、Imageコンポーネントを使うと、Next.jsがリサイズした画像が自動で適用されるため、別の画像を選ぶことは出来ません。
別の画像を使うことが必須なら、imgタグやpictureタグで対応する必要があります。
※ 任意の画像を選ぶ方法があったら教えてください。
使い方
大まかに3通りあります。
- static importした画像を使う場合
- fill属性を使う場合
- 上記以外の場合
上記の使い方によって、widthやheight等、必須になるプロパティが変わります。
imgタグのsrcset、sizesが出てくるので、知らない方はMDNの画像埋め込み要素 を事前に見ておくと理解しやすいと思います。
それでは、幅1230px、高さ501pxの画像の画像を例に、簡単な使い方を見ていきます。
注意点
imgタグのwidthとheightは、intrinsic size(実際の画像のサイズ)で指定を行います。しかし、Imageコンポーネントではrendering size(表示するサイズ)で指定することになっています。必ずしも実画像のサイズを設定する必要はありません。
また、ここで紹介する例は、いずれも画像のサイズが固定されているため、自動生成される画像セットが限定的になっています。Imageコンポーネントのメリットをより活かすには、もう少し手を加える必要があり、それはsizesプロパティで解説します。
1. static importした画像を使う場合
cssモジュールのように、画像をモジュールとしてimportして使う方法です。この場合、widthとheightは自動で判別してくれるため、Imageコンポーネントのwidthとheightプロパティを指定しなくてもエラーになりません。
ただ、エラーにならないだけで、上記注意点のとおり、表示したいサイズを指定することになっています。別のサイズで表示したい場合はwidthとheightを指定してください。
import Image from "next/image";
import pic from "/public/home/sakura.jpg";
export default function Page() {
return (
<div>
<Image src={pic} alt="sakura" />
</div>
);
}
実際にブラウザに表示される画像のソースを確認すると、こんな感じです。
<img
alt="sakura"
loading="lazy"
decoding="async"
data-nimg="1"
width="1230"
height="501"
style="color: transparent;"
srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fsakura.fb75c0d7.jpg&w=1920&q=75 1x, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fsakura.fb75c0d7.jpg&w=3840&q=75 2x"
src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fsakura.fb75c0d7.jpg&w=3840&q=75">
loading="lazy"
になっています。これでビューポートに入ったタイミングで画像がロードされます。また、srcsetに自動生成された画像が設定されているのが確認できます。1x、2xといった画素密度記述子がついており、レスポンシブ対応用の画像というより、高解像度ディスプレイに対応した画像セットであることが分かります。
2. fill属性を使う場合
fillは親要素のサイズに合わせて画像を表示します。widthとheightは指定しません。親要素はposition: "relative", position: "fixed", or position: "absolute"
のいずれかである必要があります。
また、static importでなく、Imageのsrcプロパティにパスを指定する形に変更しています。ただ、これは別の使い方を示すためだけなので、static importの時でもfillプロパティは使えます。
import Image from "next/image";
export default function Page() {
return (
<div style={{ width: "600px", height: "250px", position: "relative" }}>
<Image src="/home/sakura.jpg" alt="sakura" fill />
</div>
);
}
ブラウザでの出力は以下のとおりです。
<img
alt="sakura"
loading="lazy"
decoding="async"
data-nimg="fill"
style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent"
sizes="100vw"
srcset="/_next/image?url=%2Fhome%2Fsakura.jpg&w=640&q=75 640w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=750&q=75 750w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=828&q=75 828w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=1080&q=75 1080w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=1200&q=75 1200w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=1920&q=75 1920w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=2048&q=75 2048w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=3840&q=75 3840w" src="/_next/image?url=%2Fhome%2Fsakura.jpg&w=3840&q=75">
こちらもsrcsetに自動生成された画像が設定されています。ただ、こちらは640w、1920wといった幅記述子となっており、レスポンシブ対応用の画像セットになります。幅記述子なので、sizesプロパティも設定されています。
sizes="100vw"
なので、ディスプレイの幅にもっとも近い画像が適用されます(厳密には、同じサイズか、ビューポートより大きい画像)。
実際にブラウザの開発画面のネットワークタブを開いた状態で、幅を450pxから1200pxまで広げていくと、画像が切り替わっていくのが確認できます。
タイプもwebpになっていますね。jpgとpngのいいとこどりのモダンなフォーマットです。
これが、「モダンな画像フォーマットで、常にデバイスに応じたサイズで提供します」ということだと理解しています。
3. 上記以外の場合
static importも使わず、fillプロパティも使わない方法です。おそらく一番一般的で直感的な使い方なのではと思います。 この場合、Imageコンポーネントのwidthとheightに、ブラウザに描写したいサイズを指定します。ここでは、幅1000px、高さ407pxとします。
import Image from "next/image";
export default function Page() {
return (
<div>
<Image src="/home/sakura.jpg" alt="sakura" width={1000} height={407} />
</div>
);
}
実際に生成されたimgタグです。
<img
alt="sakura"
loading="lazy"
width="1000"
height="407"
decoding="async"
data-nimg="1"
style="color:transparent"
srcset="/_next/image?url=%2Fhome%2Fsakura.jpg&w=1080&q=75 1x, /_next/image?url=%2Fhome%2Fsakura.jpg&w=2048&q=75 2x"
src="/_next/image?url=%2Fhome%2Fsakura.jpg&w=2048&q=75">
static importの例と同じく、高解像度ディスプレイ向けの画像セットが生成されていることがわかります。
まとめ
いずれの使い方でも、loading="lazy"
がデフォルトで適用されます。また、固定サイズで表示するような使い方でも、ある程度は自動で画像セットを生成してくれることが分かりました。
次に、他のプロパティを指定してloading方法を変更したり、レスポンシブ対応用に生成する画像セットを制御する方法を見ていきます。
sizesプロパティ
役割
ドキュメントには、sizesプロパティの役割は2つあると記載されています。
- Imageが自動生成した画像セットの中から、ブラウザにどの画像をダウンロードするか伝える
- Imageがどのような画像セットを生成するか制御する
また、ドキュメントには「fillを使う場合」、「レスポンシブなstylingをしている場合」、sizesの指定はパフォーマンスを大きく改善させる可能性があると記載されています。
使い方
imgタグのsizesと同じです。以下のように、「メディア・クエリ」 + 「半角スペース」 + 「画像用スペース(vwもしくはpx)」をカンマ区切りで記載します。 一番最後のセットは、いずれのクエリにも合致しないケースに適用されるので、クエリの記載はしません。
sizes="(max-width:480px) 90vw,(max-width:1200px) 75vw,50vw"
この書き方だと、画面幅が480px以下の場合はビューポートの9割の幅の画像を使用、1200px以下なら7割5分、以外なら5割の画像を使用、という意味になります。
また、役割の2点目にあるように、この記載をベースに自動で画像生成してくれます。
使用例1: 固定幅の画像
ディスプレイの幅を問わず、表示する画像の幅を固定する場合を例にしてみます。3. 上記以外の場合の使い方で解説します。
画像は、同じ幅1230px、高さ501pxの画像を使います。
import Image from "next/image";
export default function Page() {
return (
<div>
<Image
src="/home/sakura.jpg" alt="sakura"
width={1230} height={501}
sizes="(max-width:480px) 90vw,(max-width:1200px) 75vw,50vw"
/>
</div>
);
}
ブラウザに表示された画像のソースです。
<img
alt="sakura"
loading="lazy"
width="1230"
height="501"
decoding="async"
data-nimg="1"
style="color:transparent"
sizes="(max-width:480px) 90vw,(max-width:1200px) 75vw,50vw"
src="/_next/image?url=%2Fhome%2Fsakura.jpg&w=3840&q=75"
srcset="/_next/image?url=%2Fhome%2Fsakura.jpg&w=640&q=75 640w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=750&q=75 750w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=828&q=75 828w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=1080&q=75 1080w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=1200&q=75 1200w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=1920&q=75 1920w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=2048&q=75 2048w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=3840&q=75 3840w" >
640w、750w、828w、1080w、1200w、1920w、2048w、3840wの画像が生成されているのが分かります。この中から、sizesのクエリに応じた幅の画像を自動取得してくれます。
ブラウザの幅を400pxから1500pxまで徐々に広げていくと、幅に応じた画像がダウンロードされていることが確認できます。画像サイズも、画面幅が小さい時には、小さいサイズ(容量)の画像が使われていることも分かります。
使用例2: レスポンシブ
今度はビューポートの幅に合わせて、画像をアスペクト比を崩さずに表示する例です。
ビューポートが600px以下なら、ビューポートの9割(90vw)の画像、1200px以下なら7割(70vw)の画像、以外なら5割(50vw)の画像を使うようにsizesを指定してみます。
コンポーネント
import Image from "next/image";
import styles from "./Image.module.css";
export default function Page() {
return (
<div className={styles.wrapper}>
<Image
src="/home/sakura.jpg" alt="sakura"
width={1230} height={501}
sizes="(max-width:600px) 90vw,(max-width:1200px) 70vw,50vw"
className={styles.responsive}
/>
</div>
);
}
css
.wrapper {
position: relative;
width: 50vw;
}
.responsive {
max-width: 100%;
height: auto;
}
@media screen and (max-width:1200px) {
.wrapper {
width: 70vw;
}
}
@media screen and (max-width:600px) {
.wrapper {
width: 90vw;
}
}
widthとheightは指定していますが、cssでmax-width:100%; height:auto;
を設定することにより、親要素の幅に応じて画像の表示サイズが変わるようにしています。
ブラウザに出力される画像のソース
<img
alt="sakura"
loading="lazy"
width="1230"
height="501"
decoding="async"
data-nimg="1"
class="Image_responsive__W6Iuq"
style="color:transparent"
sizes="(max-width:600px) 90vw,(max-width:1200px) 70vw,50vw"
src="/_next/image?url=%2Fhome%2Fsakura.jpg&w=3840&q=75"
srcset="/_next/image?url=%2Fhome%2Fsakura.jpg&w=640&q=75 640w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=750&q=75 750w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=828&q=75 828w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=1080&q=75 1080w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=1200&q=75 1200w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=1920&q=75 1920w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=2048&q=75 2048w, /_next/image?url=%2Fhome%2Fsakura.jpg&w=3840&q=75 3840w"
>
640w、750w、828w、1080w、1200w、1920w、2048w、3840wの画像が生成されています。
画面幅1920pxの場合
ビューポートが1200pxを超えているので、画像はビューポートの50%の大きさで表示されています。また、実際の画像は1080w用にリサイズされたものが適用されたことが確認できます。
1920pxの50vwは960pxなので、画像セットの中で960pxに一番近い大きな画像が適用されています。仕様どおりですね。
画面幅1200pxの場合
1200pxなので、メディア・クエリの2つ目を満たします。画像はビューポートの7割で表示されています。使われている画像のサイズは、同じく1080w用のものが使われています。1200px*0.7は840pxなので、仕様通りかと思います。
画面幅600pxの場合
600pxは最初のメディア・クエリを満たすため、画像はビューポートの9割で表示されています。600pxの9割は540pxなので、640w用の画像が使用されています。
まとめ
sizesを設定することで、より多くの画像セットを自動生成してくれることが確認できました。実際に手動でこれだけ準備するのは大変ですし、便利さが分かると思います。 また、デバイスのビューポートに応じて適切な画像を適用してくれるので、imgタグやpictureタグで切り替えるのと比べ、だいぶ手間が省けます。
priorityプロパティ
役割
Imageコンポーネントを使うと、全てloading="lazy"
が設定されていました。これにより、画像はレンダリングの優先順位が下げられ、非同期的に読み込まれます。
ページのロード時間を短縮するためによく使われます。
ただし、lazyにしないほうが良い場面があります。初期表示のビューポート内にある、最もサイズの大きな要素の読込が完了する時間はLargest Contentful Paint(LCP) として、Core Web Vitalsで指標が定められています。lazyで画像が非同期で読み込まれてしまうと、読込が遅延しLCPが悪化してしまう場合があります。
なので、初期表示にスクロールしなくても表示される位置にある画像のうち、最も画像サイズ大きなものについては、priorityを付けた方がよいとされています。なお、priorityは複数の画像に設定しても問題ありません。公式ドキュメントによると、「デバイスサイズによって異なる画像が最大サイズとなる場合があるので、可能性がある画像にそれぞれpriorityを付けることが適切な場合もある」とのことです。
使い方
Imageコンポーネントにprirotityプロパティを設定すると、lazyローディングが無効になります。加えて、Next.jsのpreloadの対象になります。別ページにいるときに、裏で事前にダウンロードしてくれるNext.jsの機能ですね。
export default function Page() {
return (
<div className={styles.wrapper}>
<Image
priority
src="/home/sakura.jpg" alt="sakura"
width={1230} height={501}
/>
</div>
);
}
出力されたHTMLを確認すると、loading="lazy"
が設定されていないことが確認できます。
<img
alt="sakura"
width="1230" height="501"
decoding="async"
data-nimg="1"
style="color:transparent"
srcset="/_next/image?url=%2Fhome%2Fsakura.jpg&w=1920&q=75 1x, /_next/image?url=%2Fhome%2Fsakura.jpg&w=3840&q=75 2x"
src="/_next/image?url=%2Fhome%2Fsakura.jpg&w=3840&q=75"
>
補足
ビューポート内の最大画像にpriorityが設定されていないと、以下のようにブラウザのコンソールでワーニングで教えてくれます。
Image with src "/home/sakura.jpg" was detected as the Largest Contentful Paint (LCP). Please add the "priority" property if this image is above the fold. Read more: https://nextjs.org/docs/api-reference/next/image#priority
~~の画像はLCPとして検出されたので、above the fold(スクロールせずに表示される位置)に表示されるなら、priorityプロパティを設定してください、と言っています。
最後に
Next.js 13のImageコンポーネントの簡単な使い方と、その便利さについて記載しました。
imgタグで同じことをやろうとすると、異なるサイズの準備がそもそも大変になりますし、srcsetやsizesなどの記述も複雑になるので、かなり強力なコンポーネントだと思います。私もドシドシ使っていこうと考えています。
ここまで書いて何ですが、、、実はこのページの画像は、Imageコンポーネントは使っていないのです。各記事のページは、mdファイルからHTMLに変換しているため、Imageコンポーネントはおろか、JSX自体使えないのです。手動で画像最適化をするのは骨が折れるので、とにかく羨ましいです。
将来的には記事ページの生成方法も変更することを視野に入れたいと思います。
参考
- Image Component and Image Optimization: https://nextjs.org/docs/basic-features/image-optimization
- next/image: https://nextjs.org/docs/api-reference/next/image
- Cumulative Layout Shift: https://web.dev/i18n/ja/cls/
- 画像埋め込み要素: https://developer.mozilla.org/ja/docs/Web/HTML/Element/img#srcset