🦤

画像最適化の舞台裏をのぞき見👀してnext/imageを使いこなす

2024/06/25に公開

はじめに

こんにちは、令和トラベルでフロントエンドエンジニアをしているyamatsumです。

令和トラベルでは、海外旅行におけるあたらしい体験を目指す海外ツアー・ホテル予約アプリ「NEWT(ニュート)」を提供しています。NEWTではWebアプリを提供しており、Webアプリのパフォーマンスにおいて、画像は非常に重要な要素です。特に、ページの読み込み速度はカスタマー体験に直結するため、画像の最適化は欠かせません。Next.jsのImageコンポーネントは、画像最適化を容易にし、Webパフォーマンスを向上させる強力なツールです。

この記事では、next/imageのコア機能を3つのセクションに分けて解説し、Next.jsで画像最適化を行う方法を具体的に説明します🗺️

※ この記事はVercel Meeetup #1で共有した内容を記事にしたものです。

next/imageの主要機能

Reactコンポーネント

Imageコンポーネントは、HTMLのimgタグをラップしたReactコンポーネントです。imgタグと同様にsrc属性で画像パスを指定し、alt属性で代替テキストを設定します。

NEWTにおけるTOPページでのnext/imageの利用例をあげます。

import Image from "next/image";
import imgSrc from "./assets/appImage.webp";

function Page() {
  return (
    <Image
      src={appImage.src}
      alt={"NEWTアプリ画面イメージ"}
      width={289}
      height={245}
    />
  );
}

生成されたimgタグをみてみます👀

<img
  alt="NEWTアプリ画面イメージ"
  loading="lazy"
  width="289"
  height="245"
  decoding="async"
  data-nimg="1"
  srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp_image_mobile.1f97f90f.webp&amp;w=384&amp;q=75 1x,
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp_image_mobile.1f97f90f.webp&amp;w=640&amp;q=75 2x"
  src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp_image_mobile.1f97f90f.webp&amp;w=640&amp;q=75"
></img>;

Next.jsのImageコンポーネントは、単に<img>タグをラップするだけでなく、画像の最適化において重要な役割を果たします。特に、srcset属性への複数の値の設定とloading="lazy"属性の付与は、Webページのパフォーマンス向上に大きく貢献します🌟

srcset属性:レスポンシブな画像表示を実現
srcset属性は、異なるデバイスの画面サイズや解像度に合わせて最適な画像を読み込むための仕組みです。Imageコンポーネントは、このsrcset属性に複数の画像のURLとそれぞれの幅を自動的に設定します。

srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp_image_mobile.1f97f90f.webp&amp;w=384&amp;q=75 1x,
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp_image_mobile.1f97f90f.webp&amp;w=640&amp;q=75 2x"

上記の例では、幅384pxの画像と幅640pxの画像が用意されており、ブラウザはデバイスの状況に応じて適切な画像を選択します。これにより、高解像度のディスプレイでは高画質な画像を表示し、低解像度のディスプレイでは低画質な画像を表示することで、データ通信量を節約し、ページの読み込み速度を向上させることができます。

loading="lazy"属性:遅延読み込みで初期表示を高速化
loading="lazy"属性は、画像の遅延読み込みを指定する属性です。Imageコンポーネントは、この属性を自動的に<img>タグに付与します。

遅延読み込みとは、画面に表示される画像のみを読み込み、画面外の画像はスクロールされるまで読み込まないという仕組みです。これにより、初期表示に必要な画像のみが読み込まれるため、ページの初期表示速度が大幅に向上します。

ここでは全てを説明しませんが、他にも最適化のために用意されたPropsは複数あります。
https://nextjs.org/docs/app/api-reference/components/image

ローカル画像の扱い

src propsにローカルの画像を指定する場合に、2通りの方法が存在します。
一つは上の例に挙げたように画像のパスを相対パスでimportして読み込む方法です。もう一つはpublicディレクトリからの絶対パスをsrc propsに直接文字列として渡す方法です。

これらの方法による違いは、まず相対パスでimportした場合のみ、widthとheightを指定しなくても自動でサイズの割り当てを行ってくれる点があります。

https://nextjs.org/docs/app/building-your-application/optimizing/images#image-sizing

さらにキャッシュの設定に違いが生まれます。

https://nextjs.org/docs/app/building-your-application/optimizing/static-assets#caching

絶対パス(publicディレクトリ)

Cache-Control: public, max-age=0

相対パス(import)

Cache-Control: public,max-age=31536000,immutable

よってキャッシュ戦略を考える場合には、画像の更新頻度などによってローカル画像の読み込み方法を変えてみても良いかもしれません

画像API

先ほどの例のsrcset属性、src属性をみてみるとパスが /_next/image から始まっていることがわかります。これは、Next.jsの画像最適化APIのエンドポイントです。/_next/image にリクエストを送ると、このAPIは様々な処理を行い、最適化された画像をレスポンスとして返します。主な役割は以下の通りです。

画像最適化APIの役割

  • 画像のリサイズ: クライアントのデバイスの画面サイズや解像度、Imageコンポーネントのwidth、height、layoutプロパティに応じて、最適なサイズの画像を生成します。

  • 画像のフォーマット変換: クライアントのブラウザがサポートする最適な画像フォーマット(WebP、AVIFなど)に変換します。これにより、ファイルサイズを大幅に削減し、読み込み速度を向上させることができます。

  • 画質の最適化: 指定された画質(qualityプロパティ)に応じて、画質を調整します。高画質な画像が必要な場合は高画質で、そうでない場合は低画質で配信することで、データ通信量を節約できます。

  • キャッシュ: 生成した画像をキャッシュすることで、2回目以降のリクエストにはキャッシュされた画像を即座に返すことができます。これにより、サーバーの負荷を軽減し、ページの表示速度を向上させることができます。

画像最適化APIへのリクエストパラメータ

/_next/imageへのリクエストには、以下のパラメータを含めることができます。

url: 最適化したい画像のURL
w: リクエストする画像の幅(ピクセル単位)
q: リクエストする画像の品質(0〜100)
これらのパラメータを組み合わせることで、様々な条件で最適化された画像を取得することができます。

先ほどのsrc属性を見てみると

/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp_image_mobile.1f97f90f.webp&amp;w=640&amp;q=75

となっているため、
url: %2F_next%2Fstatic%2Fmedia%2Fapp_image_mobile.1f97f90f.webp
w: 640
q: 75
で配信されていることがわかります。画像APIとして配信されているため直接アクセスして画像を確認することもできます

https://newt.net/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp_image_mobile.1f97f90f.webp&w=640&q=75

つまりこれはReactコンポーネントとしてのnext/imageを利用せずとも、画像最適化APIのみだけで利用することも可能だといえます。これを応用すればより詳細にカスタマイズ可能なアートディレクションを含む独自コンポーネントの作成も可能になります。(※ ただし利用できるパラメータはAPIによってバリデーションがかかっています)

表示される画像の幅 ≠ 読み込まれる画像の幅

wのパラメータをよく見てみるとImageコンポーネントで指定した width="289"と異なる値に変換されることに気づきます。最適化に用いるwパラメータの値は、deviceSizesimageSizesの配列によって決定されます。これらの配列はデフォルトでは以下の値が利用されています。

next.config.js
module.exports = {
  images: {
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
}

Imageコンポーネントで指定したwidthに一番近い大きい方の値と、Retina向けに2倍した値に一番近い大きい方の値が利用されるため、今回の場合だと289に近い384と、2倍した時の578に一番近い640が採用されています。

画像最適化

Next.jsは画像の最適化ライブラリにSharpを利用しています。Sharpはlibvipsという高性能な画像処理ライブラリをベースにしており、非常に高速な処理が可能です。

制限とコスト

ここからは気になるお金の話です🙈
便利なnext/imageですが、プランによって利用できる画像の数に制限があります。

Hobbyプラン Proプラン
画像数 1000 5000

Proプランはその後、1000枚ごとに$5の追加料金が発生します。

https://vercel.com/docs/image-optimization/limits-and-pricing

開発するサービスの特性によっては、大量の画像を扱う必要もあると思います。
また提供するプラットフォームがWebだけでなく、アプリも扱い必要がある場合、画像のホスティングをNext.jsに任せるよりも画像配信用のCDNに任せて、WebもアプリもCDNで管理される画像を利用した方が効率的な場合もあります。

そんな時に利用するのがImage Loaderになります。

https://nextjs.org/docs/pages/building-your-application/optimizing/images#loaders

弊社では画像配信用のCDNとしてimgixを利用しています。
imgixを利用する場合のcustom loaderの設定は以下のようになります。

// Demo: https://static.imgix.net/daisy.png?format=auto&fit=max&w=300
export default function imgixLoader({ src, width, quality }) {
  const url = new URL(`https://example.com${src}`)
  const params = url.searchParams
  params.set('auto', params.getAll('auto').join(',') || 'format')
  params.set('fit', params.get('fit') || 'max')
  params.set('w', params.get('w') || width.toString())
  params.set('q', (quality || 50).toString())
  return url.href
}
おまけ:microCMSとnext/imageについて

国内で広く利用されているヘッドレスCMSとして代表的なのがmicroCMSです。
最近エイチームのグループ参画で話題となったmicroCMSですが、令和トラベルでも旅行ガイドの記事管理に利用させていただいています。
そんなmicroCMSが提供している画像APIですが、imgixがベースとなっているためほぼimgixのloaderを流用してcustom loaderとして利用できます。

https://blog.microcms.io/nextjs-picture-imgix/

さて気になるのがcustom loaderを利用した場合、課金対象となるnext/imageの利用枚数としてカウントされるのでしょうか?
ドキュメントにはこのように記載されています

Image Optimization pricing is dependent on your plan and how many unique source images you have across your projects during your billing period.
...
A source image is the value that is passed to the src prop.

つまりReactコンポーネントのnext/imageのsrc propsに渡した画像の枚数が、課金対象としてカウントされるという風に読み取れます。では、cutom loaderを利用した場合でもsrc propsにリモート画像のURLを渡している以上、カウントされてしまうのでしょうか?答えはNoです。
ドキュメントに見当たらなかったのですが(見落としていたらすいません)、custom loaderを利用している場合にはカウントされておらず、loaderを指定せずにbuilt-inの画像最適化APIを利用している場合にのみカウントされているようです。

よって個数が少なく、変更頻度が低い画像はリポジトリ内に配置してbuild-inの画像最適化APIを利用して、それ以外は画像配信用のCDNで管理しつつcustom loaderを利用するなど、チームの中でルールを決めてコストコントロールすることが大切になります。

また画像APIの説明にて「Reactコンポーネントとしてのnext/imageを利用せずとも、画像最適化APIのみだけで利用することも可能」と記載しました。つまりsrc propsとしては指定していないけれども、build-inの画像最適化APIは利用している場合においてもカウントされるようです。

まとめ

next/imageが裏でどのように画像の最適化が行われているかをみてみました。仕組みを理解することで、Lighthouseのスコアやコストコントロールに活かせそうです。

令和トラベル Tech Blog

Discussion