🎉

next/imageを使ってビルド時に画像を最適化する方法

2022/05/29に公開

はじめに

2022 年 5 月現在、next/imagenext export で使う場合、
画像プロバイダーを使って外部 URL を利用する方法しかありません。
(next/image のデフォルトは、Nodejs サーバーを使用した画像最適化 API のもと行われるため)

しかし、シンプルなウェブサイトを構築する場合や Node.js サーバーを使用せずに構築したい場合にこれは不便です。

よってこの問題を解決するためのライブラリを開発しました。
next/image を使ってビルド時に画像を最適化するソリューションをお探しの方はこちらをぜひお使いください!

  • GitHub

https://github.com/dc7290/next-export-optimize-images

  • ドキュメントサイト

https://next-export-optimize-images.vercel.app

特徴

主な特徴としまして、以下があります。

  • ビルド時に画像を最適化
  • next/image の全オプションが利用可能
  • 拡張子の変換が可能
  • sharp を使用し高速に動作
  • キャッシュにより同じ最適化の繰り返しを防止

使い方

  1. パッケージをインストールします。
yarn add -D next-export-optimize-images
  1. next.config.jsにプラグインを追加します。
next.config.js
const withExportImages = require('next-export-optimize-images')

module.exports = withExportImages({
  // write your next.js configuration values.
})
  1. package.json のビルドコマンドを変更します。
package.json
{
-  "export": "next build && next export",
+  "export": "next build && next export && next-export-optimize-images",
}
  1. 通常通り next/image を使用します。
import Image from 'next/image'

<Image src="/images/img.png" width={1920} height={1280} alt="" />
// Or import as follows
<Image src={require('./img.png')} alt="" />

設定

デフォルトの動作は必要に応じて変更可能です。
ルートに export-images.config.js を作成します。

export-images.config.js
/**
 * @type {import('next-export-optimize-images').Config}
 */
const config = {
  // your configuration values.
}

module.exports = config

詳細はこちらのドキュメントサイトをご覧ください。

使用例

ここではいくつか使用例を紹介していきます。
ただ、next/image の使い方と基本的に同じですので、詳しくはこちらの公式ドキュメントをご覧ください。

placeholder を使用する

<Image placeholder="blur" src="/images/img.png" width={1920} height={1280} alt="" />
// Or import as follows
<Image placeholder="blur" src={require('./img.png')} alt="" />

layoutfill に設定する

<Image layout="fill" objectFit="cover" src="/images/img.png" width={1920} height={1280} alt="" />
// Or import as follows
<Image layout="fill" objectFit="cover" src={require('./img.png')} alt="" />

独自の loader を設定する

CMSImage.tsx
import { ImageLoaderProps, ImageProps } from 'next/image'
import { FC } from 'react'

type Props = ImageProps

const CMSLoader = ({ src, width, quality }: ImageLoaderProps) => {
  const url = new URL(normalizeSrc(src))
  const params = url.searchParams

  params.set('auto', params.get('auto') || 'format')
  params.set('fit', params.get('fit') || 'max')
  params.set('w', params.get('w') || width.toString())

  if (quality) {
    params.set('q', quality.toString())
  }

  return url.href
}

const CMSImage: FC<Props> = (props) => {
  return <Image loader={CMSLoader} {...props} />
}

export default CMSImage

内部構造

ここからは内部でどういった処理をしているのかが気になる人向けに内部構造を紹介します。
ライブラリを使用するために必要な知識ではないのでご安心ください。

処理手順

まず、どのような処理手順があるのか、大まかに説明します。

  1. next/image をインポートする際に、next/image をラップするカスタムコンポーネントをこのライブラリから自動的に読み込むように webpack の設定を変更する。
  2. next/imageloader から最適化する画像の情報を受け取り、JSON ファイルに書き出す。
  3. next exportの後、先ほどエクスポートした JSON ファイルに基づいて画像を最適化する。

また、next dev すると、loader はほぼそのままの文字列を元の画像で返すので、ビルドに時間がかかりません。

ここからは、より深く解説していきます。

next/imageを取り込む際

import Image from "next/image";

このように next/image コンポーネントをインポートすると、自動的に next-export-optimzie-images/dist/image にエイリアスされます。
これは、webpack のエイリアス機能を利用しています。 (https://webpack.js.org/configuration/resolve/#resolvealias)

next/imageloader をカスタマイズする。

このライブラリの画像コンポーネントは、カスタマイズされた loader を内部で設定しています。
これは next/image 内で srcsrcSet などを実際にレンダリングする際に利用されます。また、ビルド時には、最適化するイメージのリストを JSON ファイルに書き出します。

このとき、layout 属性、sizes 属性、placeholder 属性などを元に、最適化するイメージのリストが作成されます。
そのため、未使用の画像が生成されることはなく、ビルド時間が無駄に長くなることはありません。

例えば、以下のように 2 つの画像コンポーネントをレンダリングするとします。

<>
  <Image src="/intrinsic.png" width={1280} height={640} alt="" />
  <Image
    src="/responsive.png"
    width={1280}
    height={640}
    alt=""
    layout="responsive"
  />
</>

このとき生成される画像は以下の通りです。

intrinsic_1280_75.png
intrinsic_2560_75.png
responsive_640_75.png
responsive_750_75.png
responsive_828_75.png
responsive_1080_75.png
responsive_1200_75.png
responsive_1920_75.png
responsive_2048_75.png
responsive_3840_75.png

画像最適化

next-export-optimzie-images を実行して、画像の最適化を開始します。
これは基本的に yarn build && yarn export の後に実行されます。

エクスポートされた JSON ファイルの情報を元に、前述の loader を通して画像が最適化されます。
このとき、最適化された画像はキャッシュデータ(画像ハッシュとファイルパス)と共に node_modules/.cache ディレクトリに一旦保存されます。
2 回目以降の最適化では、この情報を元に最適化をスキップするかどうかが決定されます。

その仕組みは次の通りです。

キャッシュデータを格納した JSON ファイルから、同じファイルパスの画像を検索する。

  • ある場合... → その画像のハッシュと比較し、違えばハッシュを更新して最適化した画像を作成。同じ場合は最適化をスキップする。
  • ない場合... → その画像のハッシュとファイルパスをキャッシュデータに格納し、最適化された画像を作成する。

おわりに

Next.js は Web アプリケーションの開発はもちろんのこと、
その使いやすさから静的サイトでも使用したくなると思います。
その際に画像の最適化で困ったらぜひこのライブラリを使用してくださればと思います!

このライブラリについての問題や機能の提案などがあれば遠慮なく
Issue や僕の Twitter にどしどしメッセージください!

https://github.com/dc7290/next-export-optimize-images/issues
https://twitter.com/d_suke_09

Discussion