📝

ブラウザだけで画像圧縮を完結させる設計思想 〜 サーバーに頼らないImage Compressorの開発記

に公開

なぜブラウザ完結にこだわったのか

画像圧縮ツールはWebに溢れている。TinyPNGやSquooshなど、優れたサービスがいくつも存在する。ではなぜ、あえて自分で作ろうと思ったのか。

きっかけは、仕事で大量の画像を処理する必要があったときのことだった。毎回オンラインサービスにアップロードして、ダウンロードして、また次の画像をアップロードして...この繰り返しが非常に煩わしかったんですよね。

加えて、業務で扱う画像には機密情報が含まれることもある。外部サーバーに送信することに、どうしても抵抗があった。

「ブラウザだけで完結する画像圧縮ツールがあれば、この問題は解決するのでは?」

そう考えて開発を始めたのがImage Compressorだ。

設計の根幹:クライアントサイド完結アーキテクチャ

このツールの最大の特徴は、すべての処理がユーザーのブラウザ内で完結することにある。

サーバーサイドで画像処理を行う場合、以下のような構成になる。

ユーザー → 画像アップロード → サーバーで圧縮処理 → ダウンロード

一方、クライアントサイド完結の場合は、

ユーザー → ブラウザ内で圧縮処理 → そのままダウンロード

サーバーを介さないことで、以下のメリットが生まれる。

  1. プライバシーの確保:画像データが外部に送信されない
  2. 高速な処理:ネットワーク遅延がない
  3. オフライン対応:一度読み込めばネット接続なしで使える
  4. サーバーコストゼロ:インフラ維持費がかからない

デメリットもある。ユーザーの端末性能に依存するため、低スペックな端末では処理が遅くなる可能性がある。しかし現代のブラウザとJavaScriptエンジンは十分に高速で、一般的な画像処理であれば問題になることは少ない。

Canvas APIを選んだ理由

ブラウザで画像を圧縮する方法はいくつかある。

  1. Canvas API(ブラウザネイティブ)
  2. WebAssembly(libvipsなど)
  3. Web Workers + 圧縮ライブラリ

今回はCanvas APIを選択した。理由は「シンプルさ」だ。

Canvas APIは、ブラウザに標準搭載されている機能で、追加のライブラリなしで画像の描画と出力ができる。canvas.toBlob()メソッドに品質パラメータを渡すだけで、JPEG/PNG/WebPへの変換と品質調整が可能だ。

canvas.toBlob(
  (blob) => {
    // 圧縮されたBlobを受け取る
  },
  'image/jpeg',  // 出力形式
  0.8            // 品質(0.0〜1.0)
);

WebAssemblyを使えばより高度な圧縮が可能だが、バンドルサイズが大きくなり、初回読み込みが遅くなる。個人開発のツールとして、手軽に使えることを優先した結果、Canvas APIという選択に落ち着いた。

ステート設計:なぜこの形にしたのか

画像圧縮ツールのステート設計は、意外と考えることが多い。

今回採用した型定義は以下のとおりだ。

interface CompressedImage {
  id: string;                    // 一意識別子
  originalFile: File;            // 元のFileオブジェクト
  originalUrl: string;           // Data URL形式の元画像
  originalSize: number;          // 元のサイズ(バイト)
  compressedUrl: string;         // 圧縮後のObject URL
  compressedSize: number;        // 圧縮後のサイズ(バイト)
  compressedBlob: Blob | null;   // 圧縮後のBlob
}

ポイントは、元画像と圧縮後の画像を両方保持している点だ。

Before/After比較を表示するためには、両方のデータが必要になる。また、品質設定を変更して再圧縮する際にも、元データがあれば何度でもやり直せる。

compressedBlobをnullable にしているのは、アップロード直後はまだ圧縮していない状態を表現するためだ。ユーザーが設定を調整してから圧縮を実行するフローを想定している。

並列処理の設計判断

複数画像を処理する際、逐次処理と並列処理のどちらを選ぶか。これも設計上の重要な判断だった。

// 逐次処理
for (const img of images) {
  await compressImage(img);
}

// 並列処理
await Promise.all(images.map(img => compressImage(img)));

結論として、並列処理を選択した。

理由は処理速度だ。Canvas APIによる画像圧縮は、メインスレッドをブロックするものの、個々の処理時間は短い。10枚の画像を処理する場合、逐次処理だと「1枚の処理時間 × 10」かかるが、並列処理なら「最も遅い1枚の処理時間」程度で完了する。

ただし、これは画像サイズが比較的小さい場合の話だ。数十MBの高解像度画像を何十枚も同時に処理しようとすると、メモリ使用量が跳ね上がり、ブラウザがフリーズする可能性がある。

現時点ではこの問題に対する対策は入れていない。将来的には、同時処理数を制限する仕組みを入れることを検討している。

クリップボード連携:なぜPNGだけなのか

このツールでは、圧縮後の画像をクリップボードにコピーする機能を実装している。ただし、PNG形式のみの対応となっている。

これはブラウザのClipboard APIの制約による。

// クリップボードに画像を書き込む
await navigator.clipboard.write([
  new ClipboardItem({
    'image/png': blob  // PNG形式のみサポート
  })
]);

Clipboard APIのClipboardItemは、現時点でimage/png形式のみを安定してサポートしている。JPEG形式を渡すと、ブラウザによっては動作しない。

この制約を回避する方法として、JPEGやWebPをPNGに変換してからクリップボードにコピーするという手もあった。しかし、それではサイズが増えてしまい、せっかく圧縮した意味が薄れる。

結局、「PNG形式のみ対応」という形で制約を明示することにした。ユーザーに制約を伝えることで、混乱を避けるという判断だ。

UI/UXの設計思想

UIデザインにおいて意識したのは、「状態の可視化」だ。

画像圧縮ツールでユーザーが最も知りたいのは、「どれくらい圧縮されたか」という結果だ。そのため、以下の情報を常に表示するようにした。

  1. Before/After画像の並列表示
  2. 元サイズと圧縮後サイズの数値
  3. 圧縮率(パーセント表示)
  4. 削減されたサイズ

また、複数画像を扱う場合は、全体の統計情報も重要になる。

  1. 処理済み枚数 / 全枚数
  2. 元の合計サイズ
  3. 圧縮後の合計サイズ
  4. 全体の圧縮率と削減量

これらの情報を一目で把握できるようにすることで、ユーザーは圧縮の効果を実感しやすくなる。


ツールURL: https://tools.easegis.jp/ja/tools/image/image-compressor

今後の展望

現在のImage Compressorには、まだ改善の余地がある。

  1. リサイズ機能の追加

    • 画像サイズの変更もセットで行えると便利だろう
  2. AVIF形式への対応

    • WebPよりも高効率な次世代フォーマット
  3. 大量画像処理時のメモリ管理

    • 処理数の制限やチャンク処理の導入
  4. PWA化

    • オフライン対応をより完全にする

個人開発ツールとして、必要に応じて少しずつ機能を追加していく予定だ。

学びと振り返り

今回の開発を通じて改めて感じたのは、ブラウザの進化だ。Canvas APIやClipboard APIなど、以前はプラグインや外部ツールに頼っていた機能が、今ではブラウザ標準で実現できる。

「サーバーに頼らない」という制約を自らに課したことで、ブラウザAPIへの理解が深まった。制約があるからこそ、工夫が生まれる。これは開発において大切な学びだったと思う。

ぜひ使ってみて、フィードバックをいただけると嬉しいです。

ツールURL: https://tools.easegis.jp/ja/tools/image/image-compressor

Discussion