博客 / 詳情

返回

基於 Squoosh WASM 的瀏覽器端圖片轉換庫

在 Web 開發中,圖片處理是一個常見需求。傳統方案要麼依賴服務端處理,要麼使用 Canvas API,但前者增加服務器負擔,後者在壓縮質量上不盡人意。Google 的 Squoosh 項目提供了基於 WASM 的高質量圖片編解碼器,但直接使用比較繁瑣。

於是我封裝了 use-squoosh,一個零依賴的瀏覽器端圖片轉換庫,通過 CDN 按需加載編解碼器,開箱即用。

為什麼需要這個庫

現有方案的侷限性

方案 優點 缺點
服務端處理 穩定可靠 增加服務器負擔、網絡開銷
Canvas API 無依賴 JPEG 質量差、不支持 WebP 編碼
直接使用 @jsquash 質量好 需要手動管理多個包、配置 WASM
在線工具 簡單 隱私風險、批量處理不便

Canvas 的質量問題

Canvas 的 toBlob()toDataURL() 方法雖然簡單,但存在明顯缺陷:

// Canvas 方式
canvas.toBlob(callback, 'image/jpeg', 0.8);

問題:

  1. JPEG 編碼器質量較差,同等文件大小下清晰度不如專業編碼器
  2. 不支持 WebP 編碼(部分舊瀏覽器)
  3. 無法精確控制編碼參數

Squoosh 的優勢

Squoosh 是 Google Chrome Labs 開發的圖片壓縮工具,其核心是一系列編譯為 WASM 的高性能編解碼器:

  • MozJPEG:Mozilla 優化的 JPEG 編碼器,同等質量下文件更小
  • libwebp:Google 官方 WebP 編解碼器
  • OxiPNG:Rust 編寫的 PNG 優化器

@jsquash 將這些編解碼器封裝為獨立的 npm 包,但直接使用需要:

  1. 安裝多個包(@jsquash/webp、@jsquash/png、@jsquash/jpeg)
  2. 手動處理 WASM 文件加載
  3. 管理編解碼器的初始化

use-squoosh 解決了這些問題。

核心設計思路

零依賴 + CDN 加載

最核心的設計決策是:不打包編解碼器,運行時從 CDN 加載

// 編解碼器通過動態 import 從 CDN 加載
const url = `${cdnConfig.baseUrl}/@jsquash/webp@${version}/encode.js`;
const module = await import(/* @vite-ignore */ url);

好處:

  1. 庫本身體積極小(< 5KB gzipped)
  2. 編解碼器按需加載,不使用的格式不會下載
  3. 利用 CDN 緩存,多項目共享同一份 WASM

加載時機:

  • 首次調用轉換函數時加載對應格式的編解碼器
  • 加載後緩存到 window 對象,頁面內複用
  • 支持預加載關鍵格式

Promise 緩存避免競態

併發場景下可能同時觸發多次加載:

// 錯誤示例:可能重複加載
async function getEncoder() {
  if (!cache.encoder) {
    cache.encoder = await import(url);  // 併發時會多次觸發
  }
  return cache.encoder;
}

解決方案是緩存 Promise 而非結果:

// 正確示例:緩存 Promise
async function getCodec(type: CodecType): Promise<any> {
  const cache = getCache();
  if (!cache[type]) {
    // 緩存 Promise 本身,而非 await 後的結果
    cache[type] = import(/* @vite-ignore */ url);
  }
  const module = await cache[type];
  return module.default;
}

這樣即使併發調用,也只會觸發一次網絡請求。

全局緩存支持多項目共享

編解碼器掛載到 window 對象:

function getCache(): CodecCache {
  if (typeof window !== "undefined") {
    const key = cdnConfig.cacheKey;
    if (!(window as any)[key]) {
      (window as any)[key] = createEmptyCache();
    }
    return (window as any)[key];
  }
  return moduleCache;  // 非瀏覽器環境回退
}

好處:

  • 同一頁面多個組件/庫使用 use-squoosh,共享編解碼器
  • 頁面導航不重新加載(SPA 場景)
  • 可配置 cacheKey 實現隔離

實現細節

格式自動檢測

當輸入是 BlobFile 時,自動從 MIME 類型檢測格式:

const FORMAT_MAP: Record<string, ImageFormat> = {
  "image/png": "png",
  "image/jpeg": "jpeg",
  "image/webp": "webp",
  // 同時支持擴展名
  png: "png",
  jpeg: "jpeg",
  jpg: "jpeg",
  webp: "webp",
};

export async function convert(
  input: ArrayBuffer | Blob | File,
  options: ConvertOptions = {},
): Promise<ArrayBuffer> {
  let buffer: ArrayBuffer;
  let fromFormat = options.from;

  if (input instanceof Blob || input instanceof File) {
    buffer = await input.arrayBuffer();
    // 自動檢測格式
    if (!fromFormat && input.type) {
      fromFormat = getFormat(input.type) ?? undefined;
    }
  } else {
    buffer = input;
  }

  // ...
}

解碼 -> 編碼流程

圖片轉換本質是:解碼為 ImageData → 編碼為目標格式。

export async function decode(
  buffer: ArrayBuffer,
  type: ImageFormat,
): Promise<ImageData> {
  switch (type.toLowerCase()) {
    case "png": {
      const decoder = await getPngDecoder();
      return decoder(buffer);
    }
    case "jpeg":
    case "jpg": {
      const decoder = await getJpegDecoder();
      return decoder(buffer);
    }
    case "webp": {
      const decoder = await getWebpDecoder();
      return decoder(buffer);
    }
    default:
      throw new Error(`Unsupported decode type: ${type}`);
  }
}

export async function encode(
  imageData: ImageData,
  type: ImageFormat,
  options: { quality?: number } = {},
): Promise<ArrayBuffer> {
  switch (type.toLowerCase()) {
    case "png": {
      const encoder = await getPngEncoder();
      return encoder(imageData);  // PNG 無損,不需要 quality
    }
    case "jpeg":
    case "jpg": {
      const encoder = await getJpegEncoder();
      return encoder(imageData, { quality: options.quality ?? 75 });
    }
    case "webp": {
      const encoder = await getWebpEncoder();
      return encoder(imageData, { quality: options.quality ?? 75 });
    }
    default:
      throw new Error(`Unsupported encode type: ${type}`);
  }
}

CDN 配置系統

支持自定義 CDN 地址和版本:

export interface CDNConfig {
  baseUrl?: string;      // CDN 基礎路徑
  webpVersion?: string;  // @jsquash/webp 版本
  pngVersion?: string;   // @jsquash/png 版本
  jpegVersion?: string;  // @jsquash/jpeg 版本
  cacheKey?: string;     // window 緩存 key
}

const defaultCDNConfig: Required<CDNConfig> = {
  baseUrl: "https://cdn.jsdelivr.net/npm",
  webpVersion: "1.5.0",
  pngVersion: "3.1.1",
  jpegVersion: "1.6.0",
  cacheKey: "__ImageConverterCache__",
};

智能緩存清除: 只有 CDN 相關配置變更時才清除緩存:

export function configure(config: CDNConfig): void {
  const cdnKeys: (keyof CDNConfig)[] = [
    "baseUrl", "webpVersion", "pngVersion", "jpegVersion",
  ];

  // 只有這些字段變更才清除緩存
  const needsClearCache = cdnKeys.some(
    (key) => key in config && config[key] !== cdnConfig[key],
  );

  cdnConfig = { ...cdnConfig, ...config };

  if (needsClearCache) {
    clearCache();
  }
}

編解碼器 URL 生成

統一管理編解碼器的包名、版本和文件路徑:

const codecConfig: Record<
  CodecType,
  { pkg: string; version: keyof CDNConfig; file: string }
> = {
  webpEncoder: { pkg: "@jsquash/webp", version: "webpVersion", file: "encode.js" },
  webpDecoder: { pkg: "@jsquash/webp", version: "webpVersion", file: "decode.js" },
  pngEncoder: { pkg: "@jsquash/png", version: "pngVersion", file: "encode.js" },
  pngDecoder: { pkg: "@jsquash/png", version: "pngVersion", file: "decode.js" },
  jpegEncoder: { pkg: "@jsquash/jpeg", version: "jpegVersion", file: "encode.js" },
  jpegDecoder: { pkg: "@jsquash/jpeg", version: "jpegVersion", file: "decode.js" },
};

async function getCodec(type: CodecType): Promise<any> {
  const cache = getCache();
  if (!cache[type]) {
    const { pkg, version, file } = codecConfig[type];
    const url = `${cdnConfig.baseUrl}/${pkg}@${cdnConfig[version]}/${file}`;
    cache[type] = import(/* @vite-ignore */ url);
  }
  const module = await cache[type];
  return module.default;
}

使用方式

基本使用

import { convert, pngToWebp, compress } from 'use-squoosh';

// 文件選擇器獲取圖片
const file = input.files[0];

// PNG 轉 WebP
const webpBuffer = await pngToWebp(file, { quality: 80 });

// 通用轉換
const result = await convert(file, {
  from: 'png',    // Blob/File 可省略,自動檢測
  to: 'webp',
  quality: 85
});

// 壓縮(保持原格式)
const compressed = await compress(file, {
  format: 'jpeg',
  quality: 70
});

配置 CDN

import { configure } from 'use-squoosh';

// 使用 unpkg
configure({ baseUrl: 'https://unpkg.com' });

// 使用自託管 CDN
configure({ baseUrl: 'https://your-cdn.com/npm' });

// 鎖定特定版本
configure({
  webpVersion: '1.5.0',
  pngVersion: '3.1.1',
  jpegVersion: '1.6.0'
});

預加載優化首屏

import { preload, isLoaded } from 'use-squoosh';

// 頁面加載時預加載常用格式
await preload(['webp', 'png']);

// 檢查加載狀態
if (isLoaded('webp')) {
  // WebP 編解碼器已就緒
}

工具函數

import { toBlob, toDataURL, download } from 'use-squoosh';

const buffer = await pngToWebp(file);

// 轉為 Blob
const blob = toBlob(buffer, 'image/webp');

// 轉為 Data URL(用於 img.src)
const dataUrl = await toDataURL(buffer, 'image/webp');

// 觸發下載
download(buffer, 'converted.webp', 'image/webp');

自託管 CDN

如果不想依賴公共 CDN,可以自託管編解碼器文件。

目錄結構要求

your-cdn.com/npm/
  @jsquash/
    webp@1.5.0/
      encode.js
      decode.js
    png@3.1.1/
      encode.js
      decode.js
    jpeg@1.6.0/
      encode.js
      decode.js

獲取文件

從 npm 下載對應版本:

# 下載 @jsquash 包
npm pack @jsquash/webp@1.5.0
npm pack @jsquash/png@3.1.1
npm pack @jsquash/jpeg@1.6.0

# 解壓並部署到 CDN

配置使用

configure({
  baseUrl: 'https://your-cdn.com/npm',
  webpVersion: '1.5.0',
  pngVersion: '3.1.1',
  jpegVersion: '1.6.0'
});

壓縮效果對比

以一張 1920x1080 的 PNG 截圖為例:

輸出格式 Quality 文件大小 壓縮率
原始 PNG - 2.1 MB -
WebP 80 186 KB 91%
WebP 90 312 KB 85%
JPEG 80 245 KB 88%
JPEG 90 398 KB 81%

WebP 在同等視覺質量下,文件大小比 JPEG 小約 25-35%。

瀏覽器兼容性

需要支持 WebAssembly 和動態 import:

瀏覽器 最低版本
Chrome 57+
Firefox 52+
Safari 11+
Edge 16+

覆蓋全球 95%+ 的用户。

與其他方案對比

特性 use-squoosh browser-image-compression 直接使用 @jsquash
包大小 < 5KB ~50KB ~2KB × 6
運行時依賴 CDN 加載 打包在內 需手動配置
WebP 支持
PNG 優化
質量控制
自動格式檢測
預加載 需手動
自定義 CDN
TypeScript

總結

use-squoosh 通過以下設計實現了易用的瀏覽器端圖片轉換:

  1. 零依賴設計:編解碼器按需從 CDN 加載,庫本身極輕量
  2. Promise 緩存:避免併發場景重複加載
  3. 全局共享:多組件/項目複用編解碼器
  4. 靈活配置:支持自定義 CDN 和版本鎖定
  5. TypeScript:完整類型定義,開發體驗好

項目已開源:https://github.com/wsafight/use-squoosh

歡迎提出 issue 和 PR。

參考資料

  • Squoosh - Google 的在線圖片壓縮工具
  • jSquash - Squoosh 編解碼器的 npm 封裝
  • WebAssembly - 瀏覽器端高性能運行時
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.