WASMビルドパイプラインで画像最適化を自動化 2025 — esbuildとLightning CSSの統合レシピ

公開: 2025年9月29日 · 読了目安: 7 · 著者: Unified Image Tools 編集部

Jamstack とエッジ配信の両立を求める Web コーダーにとって、「画像をビルド時にどこまで自動生成し、どこから配信時に委ねるか」は悩みの種です。2025 年現在、WASM (WebAssembly) を利用した高速コンパイルと最適化ライブラリが充実し、Node.js 環境でも GPU に頼らずに高品質な派生 assets を生成できるようになりました。本稿では、esbuild と Lightning CSS を中心に、Squoosh CLI や AVIF エンコーダを WASM で組み合わせるビルドパイプラインを構築し、CI/CD に落とし込むためのレシピを紹介します。

TL;DR

  • WASMバンドルの3層構成: (1) esbuild で TypeScript + WASM プラグインをコンパイル、(2) Squoosh CLI の呼び出しを Worker Thread 化、(3) Lightning CSS で image-set() を自動生成。
  • 宣言的な画像派生ルール: assets.manifest.json でサイズ・形式・品質を定義し、esbuild のプラグインで読み込む。
  • CIでの完全再現性: Git LFS よりも軽量な .uasset キャッシュを Artifact として再利用しつつ、ハッシュが合わない場合は自動再生成。
  • ローカル検証ファースト: Playwright と 画像比較スライダー を組み合わせたビジュアル差分で、ビルド済み画像のアーティファクトを検出。
  • フォールバックと署名: 生成された WebP/AVIF には 高度変換 (AVIF/WebP) で ICC プロファイルを統合し、C2PA 署名済みの PNG をフォールバックにする。

パイプライン全体像

レイヤ役割主なツール成果物検証ポイント
Source画像派生設定とベースアセット管理assets.manifest.json, Git LFS入力PNG/TIFF, メタデータICC, 著作権情報が揃っているか
Build (WASM)WASM での変換処理esbuild, Squoosh CLI, AVIF-wasmAVIF/WebP/JPEG XL, asset-map.jsonサイズ減、品質閾値、メタ保持
StyleCSSへの組み込みLightning CSS, PostCSS*.css, image-set()content-type, sizes 一貫性
Observability差分検証と署名画像比較スライダー, 高度変換 (AVIF/WebP), C2PA CLIスナップショット, 署名済みPNGΔE2000, 署名検証結果
DeliveryCDN/Edge 配信Vercel/Cloudflare, R2/S3キャッシュ済み派生MIME, Cache-Control, ETag

宣言的マニフェストで派生を統制

単なる npm スクリプトでは派生パターンが属人的になるため、JSON マニフェストでルール化します。

{
  "hero-landing": {
    "source": "assets/hero-source.tif",
    "variants": [
      { "format": "avif", "width": 1600, "quality": 0.82 },
      { "format": "webp", "width": 1200, "quality": 0.88 },
      { "format": "jpeg", "width": 800, "quality": 0.92, "icc": "profiles/display-p3.icc" }
    ],
    "responsive": {
      "breakpoints": ["(min-width: 1280px) 1200px", "(min-width: 768px) 65vw", "100vw"],
      "density": ["1x", "2x"]
    }
  }
}

このファイルを esbuild プラグインが読み込み、WASM で並列処理します。

esbuild プラグインの実装

// build/plugins/image-pipeline.ts
import { Plugin } from 'esbuild'
import { readFile } from 'node:fs/promises'
import { runPipeline } from '../wasm/run-pipeline'

export const imagePipeline = (): Plugin => ({
  name: 'image-pipeline',
  setup(build) {
    build.onStart(async () => {
      const manifest = JSON.parse(await readFile('assets.manifest.json', 'utf-8'))
      await runPipeline(manifest)
    })
  }
})

runPipeline では Squoosh CLI を Worker Thread で呼び出し、CPU コア数に応じて同時処理数を制御します。

// build/wasm/run-pipeline.ts
import { Worker } from 'node:worker_threads'
import os from 'node:os'

const workerCount = Math.max(1, Math.min(os.cpus().length - 1, 4))

export async function runPipeline(manifest: Record<string, any>) {
  const entries = Object.entries(manifest)
  await Promise.all(entries.map(([id, config]) => dispatchWorker(id, config)))
}

function dispatchWorker(id: string, config: any) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(new URL('./workers/image-worker.ts', import.meta.url), {
      workerData: { id, config }
    })
    worker.once('message', resolve)
    worker.once('error', reject)
    worker.once('exit', code => code !== 0 && reject(new Error(`worker ${id} exited ${code}`)))
  })
}

Worker 内では wasm バイナリをキャッシュして初回ロードを短縮します。

// build/wasm/workers/image-worker.ts
import { parentPort, workerData } from 'node:worker_threads'
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)

const squoosh = require('@squoosh/lib')
const avif = require('@squoosh/avif')

(async () => {
  const { id, config } = workerData
  const { source, variants } = config
  const jobs = variants.map(async variant => {
    const image = await squoosh.ImagePool.fromPath(source)
    await image.encode({
      [variant.format]: {
        quality: Math.round(variant.quality * 100),
        effort: 7
      }
    })
    await image.save(`public/images/${id}-${variant.width}.${variant.format}`)
  })
  await Promise.all(jobs)
  parentPort?.postMessage({ id, ok: true })
})()

Lightning CSS で CSS 宣言を自動生成

Variant を CSS に反映するために、Lightning CSS のトランスフォーマを組み込みます。

// build/styles/picture-plugin.ts
import { readFileSync } from 'node:fs'
import { transform } from 'lightningcss'
import manifest from '../../asset-map.json'

export function injectImageSets(cssPath: string) {
  const css = readFileSync(cssPath)
  const result = transform({
    filename: cssPath,
    code: css,
    drafts: { nesting: true },
    visitor: {
      Rule(rule) {
        if (!rule.selectors.includes('.hero-visual')) return
        const sources = manifest['hero-landing'].imageSet
        rule.declarations.push({
          kind: 'declaration',
          property: 'background-image',
          value: {
            type: 'value',
            value: `image-set(${sources.join(', ')})`
          }
        })
      }
    }
  })
  return result.code
}

ビルド後に生成した asset-map.json を参照し、image-set() 宣言と @media ブロックを同期させます。Lightning CSS は WASM 版が高速なため、開発サーバでもホットリロードに耐えます。

CI での差分検証と署名

WASM ビルドは高速ですが、意図しない破損が起こる可能性があるため CI での検証が不可欠です。

  1. ビジュアル差分: Playwright でヒーローセクションをキャプチャし、画像比較スライダー の CLI で ΔE2000 を算出。
  2. メタデータ保持チェック: ExifTool と 高度変換 (AVIF/WebP) で ICC プロファイルと XMP の有無を検査。
  3. C2PA 署名: cai CLI で PNG フォールバックに署名し、結果を asset-map.json に追加。
# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build:images
      - run: npm run test:visual
      - run: npm run sign:fallback
      - uses: actions/upload-artifact@v4
        with:
          name: wasm-assets
          path: public/images

Artifact 再利用により、再ビルド時間を 40% 以上削減できます。キャッシュがハッシュと一致しない場合は再生成し、Slack に通知する仕組みを追加しましょう。

Edge 配信でのベストプラクティス

  • エッジキャッシュキー: クエリパラメータによる ?format= 切り替えを避け、Accept ヘッダー駆動のネゴシエーションに統一。
  • ヘッダー整合性: Cache-Controlpublic, max-age=31536000, immutable を基本に、C2PA 署名 PNG だけ must-revalidate を付与。
  • 障害時フォールバック: WASM 変換に失敗した場合、高度変換 (AVIF/WebP) で生成した高品質 JPEG を返す Lambda@Edge を用意。
  • モニタリング: PerformanceObserverLargestContentfulPaint を収集し、生成画像のサイズ変動が LCP を悪化させていないか確認。

実装チェックリスト

  • [ ] assets.manifest.json に派生ルールが明示されている
  • [ ] WASM ワーカーが CPU コア数に応じてスロットリングされている
  • [ ] asset-map.json に形式・サイズ・ハッシュが記録されている
  • [ ] Playwright + 画像比較スライダー で視覚差分テストが自動化されている
  • [ ] 高度変換 (AVIF/WebP) による ICC 埋め込みと署名がリリースフローに含まれている
  • [ ] CDN で Accept ネゴシエーションとフォールバックが検証済み

まとめ

WASM ベースのビルドパイプラインは、従来のネイティブ依存型と比べてセットアップが容易で、クラウド CI でも一貫した結果を得やすい利点があります。esbuild と Lightning CSS を中心に、Squoosh や AVIF wasm エンコーダを組み込むことで、Web コーダーは画像最適化をビルド時に完結させつつ、配信時の負荷を最小化できます。自動検証と署名プロセスを加えることで、パフォーマンスと信頼性の双方を満たすパイプラインを構築し、グローバルなローンチにも耐えうる体制を整えましょう。

関連記事

圧縮

損失管理型ストリーミングスロットリング 2025 — AVIF/HEIC帯域制御と品質SLO

AVIF/HEICなどの高圧縮フォーマットを配信する際に、帯域スロットリングと品質SLOを両立させるためのストリーミング制御と監視手法を整理。

ワークフロー

Service Worker画像プリフェッチ予算管理 2025 — Priority RulesとINP健全化の実践

Service Workerでの画像プリフェッチを数値管理し、INPや帯域を悪化させずにLCPを改善するための設計ガイド。Priority Hints、Background Sync、Network Information APIの統合手法を解説。

圧縮

Edge画像配信オブザーバビリティ 2025 — Web制作会社のSLO設計と運用手順

Edge CDNとブラウザでの画像配信品質を観測するためのSLO設計、計測ダッシュボード、アラート運用をWeb制作会社向けに詳解。Next.jsとGraphQLを使った実装例付き。

Web

CDNサービスレベル監査 2025 — 画像配信SLAを可視化する監査基盤

マルチCDN環境で画像SLAを証明するための監査アーキテクチャ。計測指標、証跡収集、ベンダー交渉に使えるレポーティング手法を紹介。

Web

Core Web Vitals 実践モニタリング 2025 — エンタープライズ案件のSREチェックリスト

エンタープライズ規模のWeb制作チームがCore Web Vitalsを継続監視するためのSRE運用テンプレート。SLO設計、メトリクス収集、インシデント対応まで包括的に解説します。

Web

Edge WASMによるパーソナライズドヒーロー画像 2025 — ミリ秒でローカル適応

エッジ上の WebAssembly でヒーロー画像をユーザー属性に合わせてリアルタイム生成するフロー。データ取得、キャッシュ制御、ガバナンスまでを包括的に解説。