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-wasm | AVIF/WebP/JPEG XL, asset-map.json | サイズ減、品質閾値、メタ保持 |
Style | CSSへの組み込み | Lightning CSS, PostCSS | *.css , image-set() | content-type , sizes 一貫性 |
Observability | 差分検証と署名 | 画像比較スライダー, 高度変換 (AVIF/WebP), C2PA CLI | スナップショット, 署名済みPNG | ΔE2000, 署名検証結果 |
Delivery | CDN/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 での検証が不可欠です。
- ビジュアル差分: Playwright でヒーローセクションをキャプチャし、画像比較スライダー の CLI で ΔE2000 を算出。
- メタデータ保持チェック: ExifTool と 高度変換 (AVIF/WebP) で ICC プロファイルと XMP の有無を検査。
- 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-Control
はpublic, max-age=31536000, immutable
を基本に、C2PA 署名 PNG だけmust-revalidate
を付与。 - 障害時フォールバック: WASM 変換に失敗した場合、高度変換 (AVIF/WebP) で生成した高品質 JPEG を返す Lambda@Edge を用意。
- モニタリング:
PerformanceObserver
でLargestContentfulPaint
を収集し、生成画像のサイズ変動が 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を使った実装例付き。
CDNサービスレベル監査 2025 — 画像配信SLAを可視化する監査基盤
マルチCDN環境で画像SLAを証明するための監査アーキテクチャ。計測指標、証跡収集、ベンダー交渉に使えるレポーティング手法を紹介。
Core Web Vitals 実践モニタリング 2025 — エンタープライズ案件のSREチェックリスト
エンタープライズ規模のWeb制作チームがCore Web Vitalsを継続監視するためのSRE運用テンプレート。SLO設計、メトリクス収集、インシデント対応まで包括的に解説します。
Edge WASMによるパーソナライズドヒーロー画像 2025 — ミリ秒でローカル適応
エッジ上の WebAssembly でヒーロー画像をユーザー属性に合わせてリアルタイム生成するフロー。データ取得、キャッシュ制御、ガバナンスまでを包括的に解説。