バッチ最適化パイプライン設計 INP/品質/スループットを両立 2025

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

“大量の画像を安全に最適化する”には、単発ツールではなくパイプライン設計が必要です。本稿は INP/UX を損なわずに運用するための設計・運用レシピを、UI/キュー/検証/監視まで丁寧に解説します。

アーキテクチャ概要

  • フロント: ドラッグ&ドロップ + 進捗 + キャンセル
  • ワーカー: 並列度を制御しつつキュー処理
  • ストレージ: 指紋付き命名で差し替えを安全に

内部リンク: 一括圧縮ツール, 高度変換

INP を悪化させないUI

  • アップロードは fetch() + AbortController で中断可能に
  • 重い処理は Web Worker/Queue に移譲し、メインスレッドは軽く
  • 進捗は「推定残り時間」より「処理数/合計数」の事実を表示

画質プリセット

  • サムネ: WebP/AVIF 品質中〜高、リサイズ優先
  • 写真: AVIF を第一候補、互換のため JPEG/WebP も生成
  • UI/ロゴ: PNG/WebP ロスレス、色数削減

変換行列(例)

  • 入力: PNG/JPEG/HEIC/TIFF → 出力: AVIF/WebP/JPEG(用途別ビット深度/ICC)
  • サイズ: 320/640/960/1200/1600px のプリセット(sizes と合致)
  • 色: P3→sRGB へ明示変換。ICC は sRGB を埋め込み

自動テスト(擬似コード)

// 画質差分の自動チェック
expect(butteraugli(original, output)).toBeLessThan(1.2)

破損検出/構造化データ

// 生成ファイルのメタ検証
expect(hasICC(output)).toBe(true)
expect(readingTime(articleMdx)).toBeGreaterThanOrEqual(1)

公開前チェック

  • [ ] LCP候補画像は sizes と一致
  • [ ] 代替テキスト/著作表記の整合
  • [ ] 指紋付きでキャッシュ破棄可能

監視とロールバック

  • 生成ジョブはID付与し、失敗時は原本にフォールバック

  • CDNログでフォーマット/サイズ別ヒット率を可視化

  • バイタル(LCP/INP)悪化時はしきい値で自動ロールバック

キュー設計とバックプレッシャ

  • 優先度キュー: サムネ/ヒーロー/低優先の3層
  • バックプレッシャ: 着信>処理能力のときは受付をスロットルし再試行へ回す
  • 可観測化: キュー長・滞留時間・失敗率をダッシュボード化
type Job = { id: string; kind: 'thumb'|'hero'|'bulk'; src: string }

並列度と再開性

  • 並列度 N は CPUコア/メモリから算出、画像解像度で動的調整
  • チャンク処理: 100件単位でコミット、失敗チャンクのみ再実行
  • 再開性: 成果物のハッシュ指紋で冪等化し、途中から再開
const pool = new WorkerPool({ size: Math.max(2, os.cpus().length - 1) })

ストレージ設計(S3/GCS)

  • オリジン: original/ に保存し改変禁止、公開用は public/
  • メタ: x-amz-meta-icc: srgb など最小メタのみ付与
  • ライフサイクル: 生成物はアクセス頻度でストレージ階層を移動

リトライ/冪等/期限切れ

  • リトライ: 指数バックオフ + ジッタ、最大試行回数を制限
  • 冪等: idempotency-keysrc + params のハッシュを用いる
  • 期限切れ: 同一キーの古い生成物は GC タスクで削除

コスト最適化

  • ホットサイズを先行生成しキャッシュヒット率を上げる
  • 品質/幅のプリセット化で失敗作成を削減
  • 高負荷時は effort を下げて処理時間短縮(画質は後追い再生成)

オブザーバビリティ

  • ログ: jobId, src, fmt, w, q, duration, bytes, error
  • 指標: p50/p95/p99 時間、成功率、再試行率、OOM 率
  • アラート: p95>既定値 or 失敗率>閾値で通知

例: ワーカー擬似コード

import sharp from 'sharp'

export async function process(job: Job) {
  const { src } = job
  const buf = await fetchBuffer(src)
  return sharp(buf)
    .withMetadata({ icc: 'sRGB.icc' })
    .resize({ width: 1200, withoutEnlargement: true })
    .toFormat('avif', { quality: 60, effort: 4 })
    .toBuffer()
}

トラブルシュート

  • OOM 発生: 並列度を下げ、巨大画像はストリーミング/タイル化
  • 画質ムラ: 目的別プリセットを導入し固定化
  • 処理詰まり: 優先度キューでヒーローを先行、低優先は遅延

FAQ

Q. リサイズ幅は細かく用意すべき?

A. sizes に揃えた有限集合で十分です。細分化はキャッシュ断片化を招きます。

Q. AVIF の effort は高い方が良い?

A. 実用では 3-6 の中庸がコスパ最適です。高すぎるとスループットを損ないます。

まとめ

“並列度の制御”と“画質プリセットの標準化”、そして“検証・監視・ロールバック”まで組み込んだら初めて本番耐性が整います。小さく始め、計測しながら段階的に最適化しましょう。

関連記事