Next.jsのnext/imageコンポーネントの使い方と注意点
2020/10/27にNext.js 10がリリースされました。変更点は公式のブログがとても分かりやすくまとまっています。Next.js Confの動画をチェックするのも良いかもしれません(演出が良い感じです)。
Next.js 10で特に注目すべきは、画像最適化をしてくれるnext/image
コンポーネント(next/image
)のビルトインサポートだと思います。この記事ではnext/image
についてまとめます。
next/imageとは
next/image
は、画像の表示を最適化してくれるコンポーネントです[1]。
画像の最適化
具体的には、以下のような最適化を行ってくれます。
- 画像を閲覧デバイスに応じてリサイズして表示してくれる(元画像が大きくともスマホでは小さくリサイズされた画像を表示)
- PNGやJPEGなどの形式の画像を自動でWebPにしてくれる(しかもWebP対応ブラウザでのみWebPを読み込む)
- 画像に
width
とheight
属性を指定していても、レスポンシブ対応してくれる(画像の縦横比が崩れない) - 遅延ロード(
loading="lazy"
属性)に対応
導入が簡単
next/image
は<img />
要素を少し書き換えるだけで導入できます。例えば、↓ このように画像を読み込んでいたとすると…
<img src="/examle.jpg" width={400} height={200} />
以下のように書き換えるだけでOKです。
import Image from 'next/image'
<Image src="/examle.jpg" width={400} height={200} />
alt
などの<img />
要素の属性はnext/image
でも同じように使用できます。これだけで画像の読み込みが最適化されるとは素晴らしいですね。
画像が最適化される仕組み
では一体どのような仕組みで画像は最適化されているのでしょうか。
実はnext/image
コンポーネント自体ではリサイズやWebP化などは行われておらず、URLの生成やレスポンシブ対応だけが行われています。最適化された画像自体の生成は、ブラウザからのリクエストを受けたサーバー側で行われます。
画像の最適化自体は、以下の2つのパターンで行われます。
Next.jsのビルトインのAPIを使うパターン
こちらがデフォルトの設定です。next/image
コンポーネントは、クライアント側では以下のようなimgタグに変換されます(分かりやすくするために簡略化してます)。
<img src="/_next/image?url=画像のURL&w=サイズ&q=クオリティ" />
srcで指定されている/_next/image
は、Next.js 10〜は予め用意されているエンドポイントで、Vercelなどにデプロイすれば特に設定をしなくても利用できます。
/_next/image
がリクエストを受け取るとimageOptimizer
という画像最適化のための関数が呼び出されます。
next-server/server/image-optimizer.ts →
imageOptimizer
は、クエリ文字列で指定されたURLの画像を読み込み、同じくクエリ文字列で指定されたクオリティやサイズに従って最適化された画像を生成します。
生成される画像の形式は「ブラウザがWebPに対応しているかどうか」等により柔軟に変わります(Acceptヘッダーを見ているようです)。
一度生成された画像はキャッシュとして保存されるため、同じ形式・サイズの画像が何度も生成されることはありません。
外部のサービスに画像最適化を任せるパターン
Cloudinaryやimgixなどの外部サービスに画像の最適化を任せることもできます。この設定はnext.config.js
で行います。
module.exports = {
images: {
loader: 'cloudinary', // Cloudinaryを使う場合
path: 'https://res.cloudinary.com/myaccount/', // CloudinaryのURLベース
},
}
詳しくはimage-optimization#loaderをチェックしてみてください。
なお、この記事の冒頭で紹介した画像最適化の種類は、Next.jsビルトインの仕組みを使って画像最適化を行うときの話になります。外部サービスに任せる場合だとサービスによってはWebPへの変換などはしてくれない可能性があります。
追記) next/imageのより詳しい仕組みについては下記の記事が参考になります。
next/imageを使うときに知っておきたいこと
ここからはnext/image
を導入する前に知っておきたいことをまとめていきます。
next.config.js
で行う
画像最適化の全体設定は画像最適化についての設定はnext.config.js
のimages
プロパティで行います。ここでの設定はプロジェクト内のすべてのnext/image
コンポーネントに反映されるイメージです。
module.exports = {
images: {
// ここ
},
}
設定項目の一覧は公式ドキュメントにまとまっています。
Image Optimization #Configuration →
外部の画像を最適化する場合はdomainsを指定しておく
例えば、Amazon S3やGoogle Cloud Storageの画像を最適化するときには、あらかじめnext.config.js
にdomains
を指定しておきます(でないと開発環境でエラーが発生します)。
module.exports = {
images: {
domains: ['storage.googleapis.com'],
},
}
Next.jsのpublic
ディレクトリ内の画像を読み込む場合にはこの設定は不要です。
基本的にwidthとheightを指定する
'next/image'コンポーネントでは基本的に幅(width
)と高さ(height
)を数字で指定するようにします。
import Image from 'next/image'
<Image src="..." width={600} height={300} />
<img />
タグと同じくpx
や%
などの単位をつけることはできませんが、next/image
ではレスポンシブに画像サイズを調整してくれます。例えば親要素の幅がwidth
より小さくなっても、縦横比を保ったまま画像を小さく表示してくれます。
なお<Image src="~" unsized={true} />
とすることでwidth
とheight
の指定を省略できますが、パフォーマンス等の観点からあまり使うべきではないと思います。
画質はコンポーネントごとに変えられる
画像のクオリティ(画質)はコンポーネントごとに1
〜100
の間で指定できます(デフォルトでは75)。例えば、より画質を落としたい場合は、以下のようにquality
をセットします。
import Image from 'next/image'
<Image src="..." quality={50} width={600} height={300} />
Vercelでnext/imageを使うときはLimitに注意
Next.jsのデプロイ先として真っ先に挙がるのがVercelだと思います。意外と知られていないような気がしますが、VercelにはFair Use Policyというガイドラインが決められています。
2020年10月時点
無料プラン(Hobby)の場合、使用できるBandwidthは100GB/月までです。
VercelではProductionデプロイの度にキャッシュが削除されるため、大きな画像をたくさん表示するようなアプリで何度もデプロイしていると、この制限をいつの間にか超えてしまう可能性があります。
WebP未対応のSafariでJEPG画像を読み込もうとすると逆に重くなる
Next.js 10のnext/imageによる画像最適化を試してみた
に書かれているように、以下の条件で画像サイズが逆に重くなってしまうことがあります。
-
next/image
でJPEG画像を読み込む - WebP未対応のSafariで表示
現時点の画像生成周りのコードを見る限り、Acceptヘッダーをもとに生成する画像のContent-Type
を決めているようです。WebP未対応のSafariではAcceptヘッダーのデフォルト値の関係から、JPEG画像を読み込む場合にPNG画像に変換されてしまいます。
これによりサイズが重くなってしまうため、現時点では注意が必要そうです[2]。
imgに直接marginを指定していると効かないことがある
next/image
ではレスポンシブ対応するために、<img>
を<div>
で囲ったような形で出力されます。
<div style="max-width: 100%; width: 100px;">
<div style="position: relative; padding-bottom: 100%;">
<img alt="react" src="/_next/image?url=hoge" style="visibility: visible; height: 100%; left: 0px; position: absolute; top: 0px; width: 100%;">
</div>
</div>
これゆえに<img>
に対してmargin
を直接当てていたりすると、margin
がイメージ通りに効かなかったりします。
該当するGitHub Issue: Margin styles are creating unexpected behaviour in the new Image component #18344
これについては今後のバージョンで問題が解消されるかもしれませんし、現時点でもCSSのFlexboxなどを使えば基本的に対応可能だと思います。
Discussion
こちらで対応されるようです。
おぉー!早速対応されたようで良かったです。情報ありがとうございます!