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

なぜブラウザ完結にこだわったのか
画像圧縮ツールはWebに溢れている。TinyPNGやSquooshなど、優れたサービスがいくつも存在する。ではなぜ、あえて自分で作ろうと思ったのか。
きっかけは、仕事で大量の画像を処理する必要があったときのことだった。毎回オンラインサービスにアップロードして、ダウンロードして、また次の画像をアップロードして...この繰り返しが非常に煩わしかったんですよね。
加えて、業務で扱う画像には機密情報が含まれることもある。外部サーバーに送信することに、どうしても抵抗があった。
「ブラウザだけで完結する画像圧縮ツールがあれば、この問題は解決するのでは?」
そう考えて開発を始めたのがImage Compressorだ。
設計の根幹:クライアントサイド完結アーキテクチャ
このツールの最大の特徴は、すべての処理がユーザーのブラウザ内で完結することにある。
サーバーサイドで画像処理を行う場合、以下のような構成になる。
ユーザー → 画像アップロード → サーバーで圧縮処理 → ダウンロード
一方、クライアントサイド完結の場合は、
ユーザー → ブラウザ内で圧縮処理 → そのままダウンロード
サーバーを介さないことで、以下のメリットが生まれる。
- プライバシーの確保:画像データが外部に送信されない
- 高速な処理:ネットワーク遅延がない
- オフライン対応:一度読み込めばネット接続なしで使える
- サーバーコストゼロ:インフラ維持費がかからない
デメリットもある。ユーザーの端末性能に依存するため、低スペックな端末では処理が遅くなる可能性がある。しかし現代のブラウザとJavaScriptエンジンは十分に高速で、一般的な画像処理であれば問題になることは少ない。
Canvas APIを選んだ理由
ブラウザで画像を圧縮する方法はいくつかある。
- Canvas API(ブラウザネイティブ)
- WebAssembly(libvipsなど)
- 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デザインにおいて意識したのは、「状態の可視化」だ。
画像圧縮ツールでユーザーが最も知りたいのは、「どれくらい圧縮されたか」という結果だ。そのため、以下の情報を常に表示するようにした。
- Before/After画像の並列表示
- 元サイズと圧縮後サイズの数値
- 圧縮率(パーセント表示)
- 削減されたサイズ
また、複数画像を扱う場合は、全体の統計情報も重要になる。
- 処理済み枚数 / 全枚数
- 元の合計サイズ
- 圧縮後の合計サイズ
- 全体の圧縮率と削減量
これらの情報を一目で把握できるようにすることで、ユーザーは圧縮の効果を実感しやすくなる。

ツールURL: https://tools.easegis.jp/ja/tools/image/image-compressor
今後の展望
現在のImage Compressorには、まだ改善の余地がある。
-
リサイズ機能の追加
- 画像サイズの変更もセットで行えると便利だろう
-
AVIF形式への対応
- WebPよりも高効率な次世代フォーマット
-
大量画像処理時のメモリ管理
- 処理数の制限やチャンク処理の導入
-
PWA化
- オフライン対応をより完全にする
個人開発ツールとして、必要に応じて少しずつ機能を追加していく予定だ。
学びと振り返り
今回の開発を通じて改めて感じたのは、ブラウザの進化だ。Canvas APIやClipboard APIなど、以前はプラグインや外部ツールに頼っていた機能が、今ではブラウザ標準で実現できる。
「サーバーに頼らない」という制約を自らに課したことで、ブラウザAPIへの理解が深まった。制約があるからこそ、工夫が生まれる。これは開発において大切な学びだったと思う。
ぜひ使ってみて、フィードバックをいただけると嬉しいです。
ツールURL: https://tools.easegis.jp/ja/tools/image/image-compressor
Discussion