はじめに
前回の記事↓
Next.jsで検索用のサイドバーを実装する ― ReactでWebアプリ開発〈5〉
前回、完成した検索ページをVercelにデプロイして一段落していたのですが、ほどなくしてVercelからこんなメールが届きました。

どうやら、Image Optimization(画像最適化)の無料枠の使用量(1000個の画像)を使い切ったとのことです。
その後、色々と調べた結果、画像最適化を無効にすれば解決することがわかりました。そこで、今回は画像最適化について調査したことやその解決策(+α)について共有していこうと思います。
Imageコンポーネントによる画像最適化
Next.jsでは、next/imageからインポートできるImageコンポーネントというものが画像の表示のために用意されています。
これは通常の<img>要素を拡張したもので、公式ドキュメントによると以下のような画像最適化を自動で行ってくれるそうです。
- サイズの最適化: WebP形式などを使用して、各デバイスに最適なサイズの画像を自動で提供する。
- 視覚的な安定性: レイアウトシフトを自動で防止する。
- ページ読み込みの高速化: ブラウザの遅延読み込み(Lazy Loading)を使用する。
- アセットの柔軟性: リモート画像でも、オンデマンドで画像のリサイズを行う。
Vercelにデプロイした場合、これらの画像最適化を行うために、Vercelに組み込まれている画像最適化機能を自動で使用するようになっています。(他の環境にデプロイする場合は、カスタムローダーの定義などが必要になるそうです。)
これに加え、ローカル画像とリモート画像のそれぞれについて、最適化後の画像はVercelが自動でキャッシュを保存してくれるようになっています。(詳しくはこちら → Image Optimization with Vercel)
これらの機能により、より高速なページの読み込みが実現できるようになっているとのことです。
使用量の制限
便利な機能なので積極的に使いたいところですが、Vercelではプランごとに最適化できる画像の枚数に制限が設けられています。
無料プラン(Hobby plan)の場合は月1000枚まで(2024/10/20現在)となっており(参考: Limits and Pricing for Image Optimization)、それを超えたImageコンポーネントの画像は以下のように表示されなくなってしまいます。

画像の枚数のカウントはSource Imagesの数によって計算されます。Source Imagesとは、Imageコンポーネントのsrcプロップに指定された値を指すそうです。
つまり、Imageコンポーネントが複数あっても、srcに指定している値が同一の場合1枚として扱われ、逆にImageコンポーネントが1つでも、srcに指定される値が可変の場合、その分だけ画像の枚数がカウントされてしまいます。
// カウントは1枚
<Image src="/hero.png" ... />
<Image src="/hero.png" ... />
// カウントは変数の変更分だけ増える
<Image src={imageUrl} ... />
今回の場合、検索結果や情報ページに表示されるジャケット画像にImageコンポーネントをそのまま使っており、srcにはSpotify Web APIから取得した画像URLを指定していたため、検索を数十回行うだけで制限の1000枚を優に超えてしまっていたのでした。
ちなみに、Source Imagesの使用量は自分のプロジェクトのダッシュボードから確認できます。(「Usage > Image Optimization」)

画像最適化を無効化
Vercelからのメールに貼られていたリンク(Managing Image Optimization Usage & Costs)に飛ぶと、以下のような解決策が挙げられていました。
srcプロップのクエリ文字列の変化の範囲を狭め、変更回数を最小限に抑える。domains/remotePatterns/localPatterns設定内のエントリ数を減らすか、より具体的なパターンに変更する。- 廃止された
domainsからremotePatternsに切り替え、パターンをより具体的にする。 <Image />タグにunoptimizedプロパティを追加して、画像最適化を画像ごとに無効にする。
今回のケースでは、4つ目の画像最適化の無効化を行っていきます。といっても、やることはImageコンポーネントのPropsにunoptimized={true}を追加するだけです。
<Image
src={image_url}
alt={`${name}のジャケット画像`}
width={64}
height={64}
unoptimized={true} // 追加
/>
無事、画像が表示されるようになりました。↓

遅延読み込みを無効化
ついでに、遅延読み込みの無効化も行おうと思います。
Imageコンポーネントは前述の通り、デフォルトで画像の遅延読み込みを行うように設定されます。(おそらくloading="lazy"を設定しているはず。)これにより、ビューポートに入るまでは画像を読み込まないようにするので、ページの読み込み速度を改善させることができます。
ただ、ページの最初に表示される画像の場合、遅延読み込みを設定していると、最初にページが表示された後に画像が遅れて表示されることがあります。今回の場合、情報を閲覧するページのジャケット画像がそれに当てはまっていました。
ローディング中のスケルトン表示↓

ロード完了後も画像は読み込み中↓

少しして画像もロード完了↓

今まで気になっていたので、この際に直しておきます。こちらも、修正方法は簡単で、priority={true}をImageコンポーネントのPropsに追加するだけです。
<Image
className="shadow-md rounded-sm md:rounded"
src={catalogData.album.images[0].url}
alt={`${catalogData.name}のカバー画像`}
width={260}
height={260}
priority={true} // 追加
unoptimized={true} // 追加
/>
これで、画像の読み込みが完了してからページが表示されるようになりました。
その他の変更
細かいことですが、Webアプリ名を"My Spotify App"という仮の名前から"MyMusic Insights"に変更しました。
というのも、Spotifyの公式のガイドラインに以下のような記述があったためです。
Naming your application
(中略)
The app name should not include 'Spotify' or be similar to 'Spotify' in sound or spelling. It shouldn't imply endorsement by Spotify, but suggesting to users that it is 'for Spotify' is acceptable.
(訳: アプリ名は'Spotify'を含むべきでなく、音やスペルが'Spotify'に似ていてもいけない。それはSpotifyによる承認を示すようなものであってはならない。ただし、利用者に'Spotify向け'であることを示すことは許される。)
趣味で開発しているものなのでそこまで厳密になる必要もないかもしれませんが、念のため。
おわりに
最近、急に涼しくなりましたね…。
