⚒️

npm scriptsで画像を最適化する(WebP変換とファイル監視を含む)

2023/09/13に公開
3

要件

  • srcディレクトリにある画像を対象に最適化する
  • 最適化した画像は構造を変えずhtdocsディレクトリに出力する
  • jpgとpngはWebPへの変換に対応する
  • WebPのフォールバックが必要な案件に備えてjpgとpngも同時に出力できるようにする
    • WebPを使わない
    • WebPを使うけど、フォールバック画像が必要
    • フォールバック画像は不要で、WebPを使う

パッケージをインストールする

画像最適化に必要なパッケージをインストールします。

npm i -D imagemin-keep-folder imagemin-mozjpeg imagemin-pngquant imagemin-gifsicle imagemin-svgo imagemin-webp

imageminだとディレクトリ構成が無視されてしまうIssueが挙がっています(マージされている気配はなさそう?)。
今回は「imagemin-keep-folder」で解消しています。

クロスプラットフォームに対応して簡潔に記述できるnpm-run-allとファイルの監視機能があるonchangeもインストールします。

npm i -D npm-run-all onchange

画像最適化の実行ファイルを作成する

scripts/imagemin.mjsを作成、以下のようにしています。
qualityはプロジェクトに合わせて調整してください。

scripts/imagemin.mjs
import imagemin from 'imagemin-keep-folder';
import imageminMozjpeg from 'imagemin-mozjpeg';
import imageminPngquant from 'imagemin-pngquant';
import imageminGifsicle from 'imagemin-gifsicle';
import imageminSvgo from 'imagemin-svgo';
import imageminWebp from 'imagemin-webp';

/**
 * WebPの使用とフォールバックのモードを指定します。
 * - 'noWebp': WebPを使用せず、元の画像のみ出力
 * - 'useIfPossible': WebPを使用する、WebPに変換できない元画像は出力する
 * - 'fallback': WebPを使用する、元の画像も出力
 */
optimizeImages('useIfPossible');

/**
 * WebPに変換可能な画像を最適化します。
 */
async function optimizeWebpConvertibleImages() {
  await imagemin(['src/**/*.{jpg,jpeg,png}'], {
    use: [
      imageminMozjpeg({ quality: 85, progressive: true }),
      imageminPngquant({ quality: [0.85, 0.9] }),
    ],
    replaceOutputDir: output => output.replace(/src\//, 'htdocs/')
  });
}

/**
 * WebPに変換不可能な画像を最適化します。
 */
async function optimizeNonWebpImages() {
  await imagemin(['src/**/*.{gif,svg}', '!src/assets/svg/*.svg'], {
    use: [
      imageminSvgo(),
      imageminGifsicle(),
    ],
    replaceOutputDir: output => output.replace(/src\//, 'htdocs/')
  });
}

/**
 * WebP画像を生成します。
 */
async function generateWebpImages() {
  await imagemin(['src/**/*.{jpg,jpeg,png}'], {
    use: [
      imageminWebp({ quality: 70 }),
    ],
    replaceOutputDir: output => output.replace(/src\//, 'htdocs/')
  });
}

/**
 * 画像を最適化します。
 * @param {string} webpMode - WebPの出力モード ('noWebp', 'useIfPossible', 'fallback')
 */
async function optimizeImages(webpMode) {
  switch (webpMode) {
    case 'noWebp':
      await optimizeWebpConvertibleImages();
      await optimizeNonWebpImages();
      break;
    case 'useIfPossible':
      await generateWebpImages();
      await optimizeNonWebpImages();
      break;
    case 'fallback':
      await optimizeWebpConvertibleImages();
      await generateWebpImages();
      await optimizeNonWebpImages();
      break;
    default:
      throw new Error('Invalid webpMode');
  }
}

optimizeImages()の引数に任意の文字列を渡すと、出力形式を切り替えられます。

  • noWebp: WebPを使用せず、元の画像のみ出力
  • useIfPossible: WebPを使用する、WebPに変換できない元画像は出力する
  • fallback: WebPを使用する、元の画像も出力

IEを無視できる環境であればuseIfPossibleでOKです。
jpgとpngはWebpに変換、svgとgifは変換できないので最適化して出力します。

npm scriptsを設定する

HTMLやCSSなどのスクリプトを載せていないのですが、画像処理に関することだけ抜き出しています。

package.json
  "scripts": {
    "start": "run-s -c dev watch",
    "build": "npm-run-all -s clean -p build:*",
    "watch": "run-p watch:*",
    "watch:image": "onchange \"src/**/*.{jpg,jpeg,png,gif,svg}\" -- npm run dev:image",
    "dev": "run-p dev:*",
    "dev:image": "NODE_ENV=development run-s -c image:*",
    "build:image": "NODE_ENV=production run-s -c image:*",
    "image:min": "node scripts/imagemin.mjs",
    "clean": "run-p clean:*",
    "clean:htdocs": "shx rm -rf \"htdocs\" && shx echo \"Done cleaning\""
  },
  • image:minで画像の最適化を実行します
  • devは開発用のビルド、buildは本番用のビルドとしています
  • run-sは順番に実行、-cはエラーが起きても最後の処理まで止まらずに実行するオプションです
  • run-p dev:*npm-run-all -s clean -p build:*のようにして、他のタスクを含めてglobパターンで実行します(cleanはhtdocsディレクトリを削除するスクリプト)
  • onchange \"src/**/*.{jpg,jpeg,png,gif,svg}\" -- npm run dev:imageで画像ファイルを監視します
  • run-s -c dev watchとすることで、開発用ビルドを順番に実行し、完了したら監視状態になります
  • htdocsディレクトリの削除にはshx(インストールが必要です) を使用しています

Discussion

codepopcodepop

webpや画像最適化の自動化で悩んでいたところ、こちらの記事を見つけてとても嬉しかったです!
一点だけご質問させていただきます。
srcの画像削除してもhtdocsの画像は削除されなくて
"build": "npm-run-all -s clean -p build:*",
を実行してみたら
ERROR: Task not found: "clean"
と表示されました。
cleanタスクは何を設定したらいいでしょうか?
お忙しいところ恐縮ですが、何卒よろしくお願いいたします。

安田学安田学

お役に立ててよかったです!

cleanに関してnpm scriptsを設定するに追記しました。
shxというライブラリでディレクトリを削除していますが、別のライブラリでも問題ありません。

codepopcodepop

ご回答いただきありがとうございます!
とても勉強になりました!