CI/CD パイプラインでの画像最適化自動化 - GitHub Actions と Sharp による実践構成
なぜ CI/CD で画像最適化するのか - 手動運用の限界と自動化の利点
画像最適化を開発者の手動作業に依存すると、品質のばらつきや最適化漏れが必ず発生します。CI/CD パイプラインに組み込むことで、すべての画像が一貫した基準で最適化され、人的ミスを排除できます。
手動運用の問題点:
- 開発者ごとに使用するツールや設定が異なり、品質がばらつく
- 締め切りに追われると最適化が省略される
- 新しいフォーマット (WebP, AVIF) への変換が後回しになる
- ファイルサイズの基準が曖昧で、巨大な画像がそのままデプロイされる
CI/CD 自動化の利点:
- すべての画像に同一の最適化ルールが適用される
- プルリクエスト時に自動チェックが走り、基準を満たさない画像をブロック
- WebP/AVIF の自動生成により、開発者は元画像のみ管理すればよい
- 最適化前後のファイルサイズ比較レポートが自動生成される
- 画像の品質メトリクス (SSIM, PSNR) を自動計測し、劣化を検知
導入効果の実例: ある EC サイトでは CI/CD に画像最適化を導入した結果、画像の平均ファイルサイズが 340KB から 89KB に削減 (74% 減)、LCP が 2.8 秒から 1.4 秒に改善、月間の CDN 転送量が 2.1TB から 0.6TB に減少しました。初期構築に 2 日、以降のメンテナンスはほぼゼロです。
GitHub Actions による画像最適化ワークフロー - 基本構成
GitHub Actions で画像最適化パイプラインを構築する基本的なワークフロー定義を紹介します。プルリクエスト時に変更された画像を検出し、自動最適化を実行します。
ワークフロー定義:
name: Image Optimizationon: pull_request: paths: ['src/images/**']jobs: optimize: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: node scripts/optimize-images.js - uses: actions/upload-artifact@v4 with: name: optimized-images path: dist/images/
変更画像の検出: git diff --name-only HEAD~1 で直近のコミットで変更された画像ファイルを特定し、変更分のみを処理対象にします。全画像を毎回処理すると実行時間が長くなるため、差分検出は必須です。
キャッシュの活用: actions/cache で node_modules と最適化済み画像のキャッシュを保持します。Sharp のネイティブバイナリは OS ごとに異なるため、キャッシュキーに runner.os を含めてください。キャッシュヒット時はビルド時間が 60-70% 短縮されます。
並列処理: 画像数が多い場合は matrix 戦略でジョブを分割するか、Node.js の Promise.all() で並列処理します。GitHub Actions の ubuntu-latest ランナーは 2 コアのため、並列度 2-4 が最適です。
Sharp による画像変換スクリプト - WebP と AVIF の自動生成
Sharp は Node.js の高性能画像処理ライブラリで、libvips をバックエンドに使用しています。CI/CD 環境での画像変換に最適で、ImageMagick の 4-5 倍高速に動作します。
基本的な変換スクリプト:
const sharp = require('sharp');const glob = require('fast-glob');const path = require('path');async function optimizeImage(inputPath) { const image = sharp(inputPath); const metadata = await image.metadata(); const outputDir = 'dist/images'; const name = path.basename(inputPath, path.extname(inputPath)); // 元フォーマットの最適化 await image.jpeg({ quality: 80, mozjpeg: true }) .toFile(path.join(outputDir, name + '.jpg')); // WebP 生成 await image.webp({ quality: 75, effort: 6 }) .toFile(path.join(outputDir, name + '.webp')); // AVIF 生成 await image.avif({ quality: 65, effort: 4 }) .toFile(path.join(outputDir, name + '.avif'));}
品質設定の指針:
- JPEG (mozjpeg): quality 75-85。80 が多くの場合で最適なバランス
- WebP: quality 70-80。JPEG と同等の視覚品質で 25-35% 小さい
- AVIF: quality 60-70。WebP よりさらに 20-30% 小さいが、エンコード時間が 5-10 倍長い
リサイズの組み込み: レスポンシブ画像用に複数サイズを生成する場合、sharp.resize() を組み合わせます。一般的には 640, 960, 1280, 1920 ピクセル幅の 4 サイズを生成し、srcset で使い分けます。
ファイルサイズの閾値チェックとレポート生成 - 品質ゲートの実装
CI/CD パイプラインに品質ゲートを設けることで、基準を満たさない画像がデプロイされるのを防ぎます。ファイルサイズの上限チェックと、最適化効果のレポート生成を自動化します。
閾値チェックスクリプト:
const MAX_SIZE = { hero: 200 * 1024, // ヒーロー画像: 200KB thumbnail: 50 * 1024, // サムネイル: 50KB icon: 10 * 1024, // アイコン: 10KB default: 150 * 1024 // その他: 150KB};function checkFileSize(filePath, category) { const stats = fs.statSync(filePath); const limit = MAX_SIZE[category] || MAX_SIZE.default; if (stats.size > limit) { return { pass: false, size: stats.size, limit }; } return { pass: true, size: stats.size, limit };}
PR コメントへのレポート出力: GitHub Actions の github-script アクションを使い、最適化結果をプルリクエストのコメントとして自動投稿します。各画像の元サイズ、最適化後サイズ、削減率を表形式で表示し、閾値超過があれば警告アイコンを付与します。
視覚品質の自動検証: ファイルサイズだけでなく、SSIM (Structural Similarity Index) を計測して視覚品質の劣化を検知します。SSIM が 0.95 未満の場合は品質設定を見直す必要があります。Sharp では直接 SSIM を計算できませんが、sharp-ssim パッケージや ImageMagick の compare コマンドで計測可能です。
失敗時の対応: 閾値チェックに失敗した場合、ワークフローを失敗させてマージをブロックするか、警告のみ出力してマージは許可するかを選択できます。初期導入時は警告モードで運用し、チームが慣れてからブロックモードに切り替えることを推奨します。
キャッシュ戦略とビルド時間の最適化 - 大規模プロジェクトへの対応
画像数が数百〜数千に達する大規模プロジェクトでは、毎回全画像を処理するとビルド時間が数十分に膨れ上がります。効率的なキャッシュ戦略とインクリメンタルビルドが不可欠です。
コンテンツハッシュによるスキップ:
const crypto = require('crypto');function getFileHash(filePath) { const content = fs.readFileSync(filePath); return crypto.createHash('md5').update(content).digest('hex');}// マニフェストファイルで処理済みハッシュを管理const manifest = JSON.parse(fs.readFileSync('.image-manifest.json'));const currentHash = getFileHash(inputPath);if (manifest[inputPath] === currentHash) { console.log('Skip (unchanged):', inputPath); return;}
マニフェストファイルに各画像のハッシュを記録し、変更がない画像の再処理をスキップします。これにより、1000 枚の画像があっても変更された 5 枚だけを処理できます。
GitHub Actions のキャッシュ設定:
node_modules:hashFiles('package-lock.json')をキーに使用- 最適化済み画像:
.image-manifest.jsonのハッシュをキーに使用 - Sharp のネイティブバイナリ:
runner.osと Sharp のバージョンをキーに含める
AVIF エンコードの高速化: AVIF のエンコードは CPU 負荷が高く、1 枚あたり 2-5 秒かかります。effort パラメータを 4 (デフォルト) から 2 に下げると速度が 2 倍になりますが、圧縮率は 5-10% 低下します。CI 環境では速度を優先し、effort: 2-3 を推奨します。
実践的な構成パターンと運用のコツ - チーム開発での導入
CI/CD 画像最適化をチームに導入する際の実践的なパターンと、運用で注意すべきポイントを紹介します。
構成パターン 1: PR 時の自動最適化 + コミット
プルリクエストが作成されると、最適化スクリプトが実行され、結果を同じ PR に自動コミットします。開発者は元画像をコミットするだけで、最適化版が自動的に追加されます。stefanzweifel/git-auto-commit-action を使えば、最適化結果の自動コミットが簡単に実装できます。
構成パターン 2: ビルド時に動的生成
最適化画像をリポジトリに含めず、ビルドパイプラインで動的に生成する方式です。リポジトリサイズを抑えられますが、ビルド時間が長くなります。Next.js の Image Optimization や Gatsby の gatsby-plugin-sharp がこのパターンを採用しています。
構成パターン 3: 専用の画像パイプライン
画像の追加・変更を検知する専用ワークフローを用意し、最適化結果を S3 や CDN に直接アップロードする方式です。アプリケーションのビルドと画像処理を分離でき、大規模プロジェクトに適しています。
運用のコツ:
- 最適化設定は
.imagerc.jsonのような設定ファイルで管理し、リポジトリに含める - 新しいフォーマットの追加 (例: JPEG XL) は設定ファイルの変更だけで対応できる設計にする
- ローカル開発環境でも同じスクリプトを実行できるようにし、CI との差異を排除する
- 画像の最大解像度 (例: 幅 2,560 px) を設定し、不必要に大きな画像のアップロードを防止する
- 月次で最適化効果のサマリーレポートを生成し、チームに共有する