- バックエンド / リーダー候補
- PdM
- Webエンジニア(シニア)
- Other occupations (18)
- Development
- Business
next/image はやっぱりすごかった - 機能紹介/パフォーマンス改善例/気をつけポイント
Photo by Jatniel Tunon on Unsplash
こんにちは、Wantedly, inc で長期インターンをしている市古です。
https://www.wantedly.com/id/igsr5
個人的な話になりますが、先日自身のポートフォリオサイトをリリースしました。
https://igsr5.dev
後からパフォーマンスチューニングの勉強をしたかったのであえて重たく実装したのですが、next/image を導入したところ想像以上にパフォーマンスが向上したのでブログにします。
目次
TL;DR
next/image の機能をいくつか紹介
画像フォーマット最適化
画像サイズ最適化
Lazy Load
良さげなキャッシュ関連のヘッダ付与
実際どうやって画像加工をしているの?
実際に next/image でパフォーマンス改善を行ってみた
パフォーマンス改善前
パフォーマンス改善後
結果
next/image 導入の気をつけポイント
Next.js というフレームワークの一機能に過ぎないこと
サーバサイドでの画像加工パフォーマンスは十分か
まとめ
TL;DR
- next/image の機能紹介
- next/image を導入すると勝手に色々いい感じになる
- 画像フォーマット/サイズ最適化, Lazy load, 良さげなキャッシュ関連のヘッダ付与 etc...
- 実際に next/image を導入してみて前後のパフォーマンスを比べてみた
- 非常に優秀だが、気をつけポイントも存在する
※ 加工処理のパフォーマンスなどには触れていません。
なお個人的に next/image のブログを書いていますが、Wantedly で実際に next/image を導入しているわけではありません。Wantedly での Next.js 移行については wantedly.com を Next.js に移行した話 - 背景・移行でやったこと で詳しく紹介しています。
next/image の機能をいくつか紹介
今回の next/image 導入で特に嬉しかった機能をいくつか紹介します。
ここで紹介しない機能については公式ドキュメントを参照してください。
画像フォーマット最適化
https://nextjs.org/docs/api-reference/next/image#acceptable-formats より
The default Image Optimization API will automatically detect the browser's supported image formats via the request's Accept header.
next/image はブラウザのサポートする画像フォーマットをみて最適な画像フォーマットに変換してくれます。具体的には Accept
ヘッダの値を見ているようです。
これらの画像は元は png, jpeg でしたが全員 webp に生まれ変わっています。(Chrome でアクセスした場合)
画像サイズ最適化
next/image に限らず、レイアウトシフトを避けるため img タグに width, height を指定した方が良いのは比較的知られた話ですが、next/image では width, height の指定が必須になっています。(layout="fill"を指定した場合を除く)
<Image
src={imgPath}
width={700}
height={475}
/>
しかし next/image におけるサイズ指定の恩恵はレイアウトシフトの対策だけではありません。他の恩恵として下記の2つが挙げられます。
- サーバサイドでの画像リサイズ
- 適切な画像サイズの算出
一つずつ説明していきます。
サーバサイドでの画像リサイズ
一つ目は「サーバサイドでの画像リサイズ」です。当たり前ですが通常の img タグだとサイズ指定をしても表示前にダウンロードする画像サイズはオリジナルのままです。next/image では自身で画像サーバを持っている(後述)のでサーバサイドで画像のリサイズを行うことができます。サーバサイドで画像リサイズが行われることによってダウンロードするファイルサイズが減ることはサイト表示的にもユーザのネットワーク帯域的にも非常に嬉しいことです。
適切な画像サイズの算出
二つ目は「適切な画像サイズの算出」です。初めて自分がこの機能を見た時、「え?width も height も指定してるのに適切な画像サイズって何?」となりました。これは next/image のすごいところでもあり、気をつけポイントでもあるのですが、next/image では必ずしも指定したサイズの画像が返ってくるとは限りません。
next/image では画像の表示領域やユーザのディスプレイ解像度に合わせて適切な画像サイズを返します。例えば高解像度の場合、指定したサイズよりも大きい画像サイズが変えるといった感じです。詳しい説明は next/imageを仕事で使う際に気をつけたい仕様 がとても参考になるのでそちらに譲ります。
個人的には img タグを利用するとき、width, height を指定した方がいいのは分かりつつ適切な画像サイズを考えるのが億劫で後回しにすることもあったので、このくらいの柔軟性を持たせつつ必須指定にしてくれるのはとてもありがたいです。もちろんケースによっては指定したサイズに正確にリサイズしてほしい時もあると思いますが、unoptimized パラメータを有効にすればそういったケースにも対応できそうです。
該当公式ドキュメント
- https://nextjs.org/docs/api-reference/next/image#width
- https://nextjs.org/docs/api-reference/next/image#device-sizes
Lazy Load
https://nextjs.org/docs/api-reference/next/image#loading より
The loading behavior of the image. Defaults to lazy.
next/image ではデフォルトで Lazy Load が有効になっています。
特徴としては Native Lazy Loading ではなく JS Lazy Loading なので Safari 等のブラウザでも Lazy Load が有効である点です。(Safari が loading
プロパティを早くサポートして欲しいというのはありますが、、)
iOS 15.3 の Safari で https://igsr5.dev/portfolio にアクセスした例。ちゃんと Lazy Load が動いているのが分かります。
良さげなキャッシュ関連のヘッダ付与
https://nextjs.org/docs/api-reference/next/image#caching-behavior
next/image が自身で持っている画像サーバ(後述)がいい感じに Cache-Control, Vary ヘッダをつけてくれます。
// // https://github.com/vercel/next.js/blob/v12.1.0/packages%2Fnext%2Fserver%2Fimage-optimizer.ts#L599-L605 より
res.setHeader('Vary', 'Accept')
res.setHeader(
'Cache-Control',
isStatic
? 'public, max-age=315360000, immutable'
: `public, max-age=0, must-revalidate`
)
詳しい説明は省略しますが、static であれば基本イミュータブルなキャッシュとして、そうでなければ 最大TTL 0 かつ再検証必須化と ETag の組み合わせで効率的にキャッシュを行ってくれます。
実際どうやって画像加工をしているの?
ここまでの内容で next/image はユーザ環境によって適切な画像サイズを算出したり、適切な画像フォーマットに変換することを紹介してきました。これらはビルド時に画像を変換するだけでは実現できない機能たちです。では実際どのように next/image は画像加工を行っているのでしょうか。
Next.js が画像配信用のエンドポイントを立ててそこから配信される
下記は next/image を用いて画像を表示した時にimg タグに指定される画像URLの例です。
https://igsr5.dev/_next/image?url=OOOO
このURLの中にある _next/image
が Next.js がデフォルトで持つ画像配信のエンドポイントです。このエンドポイントがリクエスト毎に画像加工やキャッシュなどを行っています。反対にビルド時には画像の加工は行われておらず全てオンデマンドで行われています。
気をつけポイントとして Vercel 以外で Next.js をデプロイするときに _next/image
もアクセス可能な状態にしておくことが挙げられます。(_next/ 配下は他にも Next.js 的に大事なエンドポイントが詰まっているのでミスらないためには _next/* で公開してしまうのもありだと思います)
個人的にはオンデマンドな画像加工はキャッシュが効いているとはいえサーバサイドでのパフォーマンスが気になるので、一度時間のあるときに測ってみたいです。
※ _next/image
から画像を配信する例のみ取り上げましたが、外部サービスで画像処理を行うこともできます。詳しくは Image Component and Image Optimization を参照してください。
実際に next/image でパフォーマンス改善を行ってみた
ここからは実際に自身のポートフォリオサイトに next/image を導入してみて前後のパフォーマンスを比較してみます。やることは下記の置き換えのみです。
// before
<img src="imgPath" />
// after (import 文は省略, OO は適当な数値)
<Image src="imgPath" width={OO} height={OO} />
また、ポートフォリオサイトで使用している画像の情報としては
- ファイルサイズ: 100 kB ~ 10 MB
- 画像サイズ: 100 px ~ 3,000 px
このようなステータスです。
ファイルサイズ、画像サイズを抑え画像のレスポンスタイムも早くなることを期待します。
パフォーマンス改善前
- 最も重い画像で10.0MB, レスポンスタイムが 11.60 s かかっています
- 画像フォーマットは png, jpeg とまちまちです
- Cache-Control の指定もありません(分かりにくいですがスクショの一番右が Cache-Control の欄です)
- スクショからは分かりませんが、Lazy Load も有効になっていないのでこれらの画像に引っ張られページ全体が遅くなっています
PageSpeed Insights の結果を見ても画像関連で怒られまくっていることが分かります。
( Properly size images が 145.2s ...)
パフォーマンス改善後
分かりやすくパフォーマンスが改善しました。
- 最も重い画像でも 57.3 kB, レスポンスタイムが 43 ms
- 画像フォーマットが全て webp 化されています(Chrome でアクセスした場合)
- Cache-Control の指定が見られます
- スクショからは分かりませんが、Lazy Load が有効になりページの読み込み速度もかなり早くなりました
PageSpeed Insights の結果を見ても画像関連の怒られがなくなっていることが分かります。(Speed Index, LCP 周りの改善が見られます。しかし TBT スコアが落ちているので、結果的にスコアが下がっている...)
結果
img タグを next/image へ置き換えただけでファイルサイズ・レスポンスタイムに大きく改善が見られました。画像サイズの最適化、Lazy Load の有効化も嬉しいポイントです。
- 最大ファイルサイズ: 10 MB → 57.3 kB
- 最大レスポンスタイム: 11.60 s → 43 ms
next/image 導入の気をつけポイント
先ほどのパフォーマンス改善で示したように next/image は非常に優秀なコンポーネントです。しかし導入時に考えなければならないポイントも存在します。それらの気をつけポイントをいくつか紹介します。
Next.js というフレームワークの一機能に過ぎないこと
next/image は非常に高機能で単体でサービスとして存在していてもおかしくないレベルです。しかし実際には next/image は Next.js の機能の一つです。next/image を利用することはそれだけで Next.js というフレームワークに依存するということです。例えば画像数の多いサービスで next/image を導入するとこの先 Next.js から別フレームワークへの移行を考える時に障害となる可能性があります。特に画像サイズ最適化で紹介したような機能は他のツールに置き換えるのが難しいので長く利用され続けるサービスでの利用は注意が必要です。(実際 Wantedly では Next.js 移行の際には next/image の利用を見送っています)
サーバサイドでの画像加工パフォーマンスは十分か
※ next/image のサーバサイド処理をちゃんと計測せずに言っています(時間がある時に測りたい)
先ほどのパフォーマンス改善例でも挙げたように next/image を導入しただけで、画像表示までの時間やファイルサイズが抑えられました。しかし要件によってはより高速な画像配信が必要になる場合があります。そういった場合は next/image より高速な画像サーバを利用するかビルド時やアップロード時などに加工を済ませておく方法を検討する必要があります。
また、next/image が用いる画像配信のエンドポイント _next/image
はあくまで Next.js アプリの一部です。あまりにも重たい画像処理が続くとアプリ全体を引きずってしまう恐れもあるのでそういった場合は loader を設定して外部の画像処理サービスに任せるという方法も検討すると良さそうです。
https://nextjs.org/docs/basic-features/image-optimization#loaders
まとめ
この記事では
- next/image の機能紹介
- next/image を導入すると勝手に色々いい感じになる
- 画像フォーマット/サイズ最適化, Lazy load, 良さげなキャッシュ関連のヘッダ付与 etc...
- 実際に next/image を導入してみて前後のパフォーマンスを比較
- 非常に優秀だが、気をつけポイントも存在する
これらの内容について説明しました。
next/image は非常に優秀で利用すればパフォーマンス改善がかなり行いやすくなりますが、要件の性質によっては無条件に導入できるわけでもなさそうです。
この記事が next/image の導入を考えている人の助けになれば幸いです。
また、個人的にはポートフォリオサイトのパフォーマンス改善はまだまだ行うつもりなので別の施策も記事に出来たらいいなと考えています。(React Suspense でコンポーネント単位の Lazy Loading とかやってみたい)