OneImage
OneImage
·
technicalwebassemblyperformance

強化されたSquooshの構築:libimagequant-wasmによる高性能ローカル画像圧縮

強化されたSquooshの構築:libimagequant-wasmによる高性能ローカル画像圧縮

GoogleSquooshのアーキテクチャとlibimagequant-wasm、Web Workers、最新のWeb APIを組み合わせて、プライバシーファーストで高性能な画像圧縮ツールを構築した方法を詳しく解説します。

Googleが2023年初めにSquooshの終了を発表したとき、Web開発コミュニティはクライアントサイド画像圧縮のための貴重なツールを失いました。OneImageでは、この機能を保存するだけでなく、強化する機会を見出しました。この記事では、プライバシー、パフォーマンス、開発者体験を優先する、本番環境対応の強化されたSquooshを構築するために採用した技術的アプローチを詳述します。

Google Squooshのアーキテクチャの理解

Google SquooshはWebAssembly(WASM)を使用して画像エンコーダー全体をブラウザ内で実行するというコンセプトを開拓しました。元のアーキテクチャは以下で構成されていました。

  • クライアントサイド処理:すべての圧縮はローカルで行われ、プライバシーを保証します
  • WebAssemblyコーデック:WASMにコンパイルされたネイティブ速度のエンコーダー(MozJPEG、OxiPNG、WebP、AVIF)
  • Web Workers:UIのブロッキングを防ぐために重い計算をオフロードします
  • Canvas API:画像操作とプレビュー生成

画期的でしたが、Squooshには限界がありました。

  • PNG圧縮はOxiPNGのみに依存し、速度よりも圧縮率を優先しました
  • 組み込みのバッチ処理機能がありませんでした
  • 一般的なユースケース用の限定的なプリセット構成
  • UIと圧縮ロジックが密結合していました

私たちの強化戦略

1. 優れたPNG圧縮のためのlibimagequant-wasmの統合

私たちの強化の要はlibimagequant-wasmであり、業界標準のpngquantライブラリのWebAssemblyポートです。このライブラリは、単純なパレット削減と比較して視覚的に優れた結果を生成する洗練された色量子化アルゴリズムを使用しています。

なぜlibimagequantなのか?

  • 知覚品質:人間の知覚に最適化された修正中央カットアルゴリズムを使用します
  • 適応パレット:画像コンテンツに基づいて2〜256色の最適なパレットを生成します
  • 透明度処理:圧縮しながらアルファチャンネルを保持します
  • パフォーマンス:WASMのおかげでほぼネイティブ速度で実行されます

実装の詳細

libimagequantを圧縮パイプラインに統合した方法は次のとおりです。

import LibImageQuant from '@fe-daily/libimagequant-wasm'; 
import * as wasmModule from '@fe-daily/libimagequant-wasm/wasm/libimagequant_wasm.js';

async function compressPNG(imageData: ImageData, level: number): Promise<Uint8Array> {
  const quantizer = new LibImageQuant({ wasmModule });
  
  // 圧縮レベル(0-10)を色数(256-2)にマッピング
  const maxColors = Math.max(2, 256 - (25.6 * level));
  
  const quantized = await quantizer.quantizeImageData(imageData, {
    maxColors: Math.floor(maxColors),
    speed: 1,          // 品質と速度のバランス
    quality: {
      min: 0,
      target: 100      // 色制限内で最高品質を目指す
    }
  });
  
  return new Uint8Array(quantized.pngBytes);
}

主要パラメータの説明

  • maxColors:パレットサイズを制御します。色が少ない=ファイルが小さいが、品質が悪化する可能性があります
  • speed:範囲1-10、1が最も遅いが最高品質です
  • quality.target:ターゲット品質しきい値(0-100)を設定します

私たちのSquooshツールは、この実装を使用して、最小限の知覚損失で60-80%の圧縮率を実現しています。

2. 堅牢なWeb Workerシステムのアーキテクト

圧縮中(特に大きな画像やバッチ操作の場合)にブラウザがフリーズするのを防ぐために、専用のWeb Workerアーキテクチャを構築しました。

// compression-worker.ts
import { EncoderOptions, CompressResult } from './squoosh-types';
import LibImageQuant from '@fe-daily/libimagequant-wasm'; 
import * as wasmModule from '@fe-daily/libimagequant-wasm/wasm/libimagequant_wasm.js';

interface CompressMessage {
  type: 'compress';
  imageData: ImageData;
  options: EncoderOptions;
}

self.onmessage = async (e: MessageEvent<CompressMessage>) => {
  const { type, imageData, options } = e.data;

  if (type === 'compress') {
    try {
      const result = await compress(imageData, options);
      self.postMessage({ success: true, result });
    } catch (error) {
      self.postMessage({
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error',
      });
    }
  }
};

async function compress(
  imageData: ImageData,
  options: EncoderOptions
): Promise<CompressResult> {
  switch (options.type) {
    case 'png':
      return await compressPNGWithQuantization(imageData, options);
    case 'jpeg':
      return await compressJPEG(imageData, options);
    case 'webp':
      return await compressWebP(imageData, options);
    case 'avif':
      return await compressAVIF(imageData, options);
  }
}

Workerの利点

  • 圧縮中の非ブロッキングUI
  • 長時間実行される操作をキャンセルする機能
  • バッチ操作の並列処理(複数のワーカー)
  • メインスレッドのメモリリークを防ぐメモリ分離

3. スマート圧縮プリセットの構築

生のエンコーダーパラメータを公開するのではなく、一般的なユースケース用に最適化された3つのプリセットを作成しました。

const PRESET_CONFIGS = {
  highQuality: {
    png: { level: 3 },     // 約200色
    jpeg: { quality: 90 },
    webp: { quality: 90 },
    avif: { quality: 85 }
  },
  balanced: {
    png: { level: 5 },     // 約128色
    jpeg: { quality: 80 },
    webp: { quality: 80 },
    avif: { quality: 70 }
  },
  minSize: {
    png: { level: 8 },     // 約50色
    jpeg: { quality: 60 },
    webp: { quality: 60 },
    avif: { quality: 50 }
  }
};

これらのプリセットは、多様な画像タイプ(写真、イラスト、スクリーンショット、UI要素)での広範なテストを通じて較正され、最適な品質とサイズのトレードオフを見つけました。

4. 効率的なバッチ処理の実装

複数の画像を圧縮するユーザーのために、進行状況追跡を備えたキューシステムを構築しました。

class BatchCompressor {
  private queue: BatchItem[] = [];
  private activeWorkers: Set<Worker> = new Set();
  private maxConcurrency = navigator.hardwareConcurrency || 4;

  async processBatch(files: File[], options: ProcessorOptions) {
    const batchId = Date.now();
    
    for (const file of files) {
      this.queue.push({
        id: `${batchId}-${file.name}`,
        file,
        status: 'pending',
        options
      });
    }

    await this.processQueue();
  }

  private async processQueue() {
    while (this.queue.some(item => item.status === 'pending')) {
      if (this.activeWorkers.size < this.maxConcurrency) {
        const item = this.queue.find(i => i.status === 'pending');
        if (item) {
          item.status = 'processing';
          await this.processItem(item);
        }
      } else {
        await new Promise(resolve => setTimeout(resolve, 100));
      }
    }
  }
}

このアプローチは以下を実現します。

  • 並列ワーカーを実行してCPU使用率を最大化
  • 同時実行を制限してブラウザのクラッシュを防止
  • ユーザーへのリアルタイム進行状況更新を提供

OneImage Squooshで、バッチ処理の動作を試してみてください。

パフォーマンス最適化

メモリ管理

大きな画像はブラウザのメモリをすぐに使い果たす可能性があります。いくつかの緩和戦略を実装しました。

async function processLargeImage(file: File): Promise<CompressResult> {
  const MAX_DIMENSION = 4096;
  const img = await loadImage(file);
  
  // 必要に応じて縮小
  let { width, height } = img;
  if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
    const scale = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height);
    width = Math.floor(width * scale);
    height = Math.floor(height * scale);
  }
  
  // より良いメモリ処理のために利用可能な場合はOffscreenCanvasを使用
  const canvas = typeof OffscreenCanvas !== 'undefined'
    ? new OffscreenCanvas(width, height)
    : document.createElement('canvas');
  
  canvas.width = width;
  canvas.height = height;
  
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0, width, height);
  
  const imageData = ctx.getImageData(0, 0, width, height);
  
  // キャンバスと画像リソースを即座に解放
  URL.revokeObjectURL(img.src);
  
  return await compress(imageData, options);
}

WASMモジュールキャッシング

WebAssemblyモジュールは積極的なキャッシングから恩恵を受けます。

let cachedWasmModule: typeof wasmModule | null = null;

async function getWasmModule() {
  if (!cachedWasmModule) {
    cachedWasmModule = await import(
      '@fe-daily/libimagequant-wasm/wasm/libimagequant_wasm.js'
    );
  }
  return cachedWasmModule;
}

これにより、初期化時間が約500ミリ秒から、以降の圧縮ではほぼ瞬時に削減されます。

ツールキットの拡張

Squooshは圧縮に焦点を当てていますが、補完的なツールの完全なスイートを構築しました。

  • Image Resize:複数のアルゴリズムを使用したスマートスケーリング
  • EXIF Remover:プライバシーのためにメタデータを削除
  • Image Overlay:透かしとブランディング
  • Image Blur:機密コンテンツの墨消し
  • Crop Tool:正確なアスペクト比クロッピング

すべてのツールは、プライバシーファースト、WASMパワード、完全にクライアントサイドという同じアーキテクチャ原則を共有しています。

ブラウザ拡張機能の統合

Webアプリアーキテクチャをブラウザ拡張機能に拡張し、次のことを可能にしました。

  • ツールバーポップアップによる即座のアクセス
  • 右クリック圧縮のためのコンテキストメニュー統合
  • タブベースの状態管理
  • プリセット設定のためのローカルストレージ

拡張機能は同じWeb WorkerとWASMインフラストラクチャを再利用し、プラットフォーム全体で一貫した動作を保証します。

デプロイとインフラストラクチャ

Cloudflareでのエッジコンピューティング

OneImage SquooshをCloudflare Pagesにデプロイし、次のことを活用しています。

  • 世界中で50ミリ秒未満の初期ロード時間のためのグローバルCDN
  • 資産のためのHTTP/3とBrotli圧縮
  • WASMモジュールのためのスマートキャッシングヘッダー
  • コールドスタートゼロ(静的資産のみ)

ビルド最適化

Next.js構成には以下が含まれます。

// next.config.ts
module.exports = {
  webpack: (config, { isServer }) => {
    // .wasmファイルのサポート
    config.experiments = {
      asyncWebAssembly: true,
      layers: true,
    };
    
    // ワーカーインポートの最適化
    config.module.rules.push({
      test: /\.worker\.(ts|js)$/,
      use: { loader: 'worker-loader' }
    });
    
    return config;
  },
  
  // 積極的なコード分割
  experimental: {
    optimizePackageImports: [
      '@jsquash/jpeg',
      '@jsquash/png',
      '@jsquash/webp',
      '@jsquash/avif'
    ]
  }
};

これにより、エンコーダーがオンデマンドでロードされ、初期バンドルが100KB未満(gzip圧縮後)に保たれます。

テストと品質保証

圧縮ロジックの包括的なテストカバレッジを維持しています。

// __tests__/advanced-compressor.test.ts
describe('AdvancedImageCompressor', () => {
  it('should compress PNG with libimagequant', async () => {
    const compressor = new AdvancedImageCompressor();
    const mockFile = createMockImageFile('test.png', 1000, 1000);
    
    const result = await compressor.compress(mockFile, {
      encode: { type: 'png', options: { level: 5 } }
    });
    
    expect(result.format).toBe('png');
    expect(result.size).toBeLessThan(mockFile.size);
    expect(result.data).toBeInstanceOf(Uint8Array);
  });
  
  it('should handle batch processing with concurrency', async () => {
    const files = Array(10).fill(null).map((_, i) => 
      createMockImageFile(`test-${i}.png`, 500, 500)
    );
    
    const startTime = Date.now();
    await batchCompress(files, { preset: 'balanced' });
    const duration = Date.now() - startTime;
    
    // 順次処理よりも速いはず
    expect(duration).toBeLessThan(10 * 1000); // 画像あたり1秒未満
  });
});

学んだ教訓

  1. WASMは本番環境対応:適切なモジュールロードとキャッシングにより、WASMパフォーマンスはネイティブアプリケーションに匹敵します
  2. Web Workersは不可欠:CPU集約型のタスクには、ワーカーへのオフロードは譲れません
  3. ユーザープリセット > 生コントロール:ほとんどのユーザーは、細かい調整よりも「良いデフォルト」を好みます
  4. メモリは重要:大きな画像でのメモリ使用量を常にプロファイルし、セーフガードを実装します
  5. プライバシーは売れる:「サーバーアップロードなし」を強調することは、ユーザーに強く響きます

オープンソースとコミュニティ

OneImage Squooshは商用製品ですが、エコシステムに貢献しています。

  • @jsquashメンテナへのバグレポートとPR
  • libimagequant-wasmのドキュメント改善
  • パフォーマンスベンチマークとベストプラクティスの共有

結論

強化されたSquooshを構築するには、libimagequant-wasmを統合するだけではありませんでした。Web Workers、メモリ管理、ユーザー体験、デプロイインフラストラクチャに関する慎重なアーキテクチャ決定が必要でした。結果は、ユーザーのプライバシーを尊重しながらプロフェッショナルグレードの圧縮パフォーマンスを提供するツールです。

今日OneImage Squooshを試すか、さらに高速なアクセスのためのブラウザ拡張機能を探索してください。同様のツールを構築する開発者にとって、この技術的な深掘りが有用な青写真を提供することを願っています。

---

参考文献とさらなる読み物