U-Code

Viteで画像を自動でWebP化する

コーディング

Viteで、.pngや.jpgの画像をビルド時に自動でWebPに変換し、リンクも書き換えるプラグインを作成したので、そのメモです。

なぜ作成したのか?

npmにはVite内の画像をWebPに変換するプラグインもありますが、以下の問題がありました。

  • すべての画像が変換対象になってしまい、OGP画像など変換したくないものも変換される
  • そのため、特定のディレクトリの画像は変換せずに出力するような仕組みがほしかった

また、本当はWebPではなくAVIFにしたいのですが、案件のサポートブラウザの関係で、AVIFはもう少し様子見です。

プラグインの仕様

今回作成したプラグインでは、次のような動作をします。

  1. publicディレクトリ内の画像は変換せず、そのまま出力する
  2. れ以外の画像はWebPに変換し、HTML内の画像リンクも書き換える
  3. 入力ディレクトリ、出力ディレクトリ、圧縮率を指定可能

動作確認はVite6.0.1、Sharp0.33.5で行いました。

事前準備(sharpのインストール)

画像の変換にはsharpというライブラリを使用するため、事前にインストールしておきます。

npm install sharp

プラグインのコード(vite.config.jsに記述)

vite.config.jsでsharpをインポートし、プラグインを設定します。

import sharp from "sharp";

プラグインの実装

function sharpWebpPlugin({ srcDir, outDir, quality = 80 }) {
  return {
    name: "sharp-webp-plugin",
    apply: "build",
    enforce: "post",
    async closeBundle() {
      const publicDir = path.resolve(srcDir, "public");
      const supportedFormats = [".jpg", ".jpeg", ".png"];

      // 画像の変換処理関数
      async function processImages(dir) {
        const files = await fs.promises.readdir(dir, { withFileTypes: true });

        await Promise.all(
          files.map(async (file) => {
            const filePath = path.join(dir, file.name);

            // publicディレクトリを除外
            if (filePath.startsWith(publicDir)) return;

            if (file.isDirectory()) {
              // ディレクトリの場合は再帰処理
              await processImages(filePath);
            } else {
              const ext = path.extname(file.name).toLowerCase();

              // JPGまたはPNGファイルの場合のみ変換
              if (supportedFormats.includes(ext)) {
                const outputFilePath = filePath.replace(srcDir, outDir).replace(ext, ".webp");
                try {
                  // outDir内にWebPを出力
                  await sharp(filePath)
                    .webp({ quality }) // 引数で指定された圧縮率を使用
                    .toFile(outputFilePath);

                  // outDir内の元の画像ファイルを削除
                  const originalOutPath = filePath.replace(srcDir, outDir);
                  if (fs.existsSync(originalOutPath)) {
                    await fs.promises.unlink(originalOutPath);
                  }
                } catch (error) {
                  console.error(`Error converting ${filePath}:`, error);
                }
              }
            }
          })
        );
      }

      // HTMLファイルのWebPパス変換関数
      async function updateHtmlToWebp(dir) {
        const files = await fs.promises.readdir(dir, { withFileTypes: true });

        await Promise.all(
          files.map(async (file) => {
            const filePath = path.join(dir, file.name);

            if (file.isDirectory()) {
              // ディレクトリの場合は再帰処理
              await updateHtmlToWebp(filePath);
            } else if (file.name.endsWith(".html")) {
              let content = fs.readFileSync(filePath, "utf-8");

              // src属性、srcset属性、url()内のパスをWebPに変換(元の拡張子を削除)
              content = content.replace(/((?:src|srcset|url\()=["']?)(?!\/?public\/)([^"')]+)\.(jpg|jpeg|png)/g, "$1$2.webp");

              fs.writeFileSync(filePath, content);
            }
          })
        );
      }

      // srcディレクトリ内の画像を変換
      await processImages(srcDir);

      // outDirディレクトリ内のHTMLファイルのパスを更新
      await updateHtmlToWebp(outDir);
    },
  };
}

以上で、Viteのビルド時に.pngや.jpgの画像が自動でWebPに変換され、指定のディレクトリ設定に従って出力されるようになります。

関連情報

このプラグインを組み込んだ、私のVite製の静的コーディング環境のGitリポジトリはこちらです。
https://github.com/u-coded/html-template

また、静的コーディング環境の作成にあたって以下のサイトを参考にさせていただきました。
https://coding-memo.work/development/1274/