WebP: Webページ圧縮形式
(purplesyringa.moe)- GitHub PagesがBrotliをサポートしていないため、HTMLを可逆WebP画像にエンコードし、ブラウザの画像デコーダとJavaScriptで復元して転送量を減らす実験
- WASM Brotliデコーダは
71 KiB~200 KBの追加コストがかかり、Compression Streams APIはgzip、deflate、deflate-rawのみをサポートするため、Brotliデコードの迂回路になりにくい - VP8L可逆WebPは予測変換、16x16ブロックごとのHuffman木の再利用、color cacheのおかげで、テキスト系データでもgzipより小さい結果を出せる
- テストHTML
439,478バイトはgzipで94,683バイト、WebPで43,182バイトまで縮小され、Brotliの37 KiBよりは大きいがgzip比で約2.2倍小さかった - Canvas 2Dのanti-fingerprintingノイズ、WebGL
readPixelsの変更、初期の空白画面、スクロール復元の問題により、実運用というよりブラウザの制約をあらわにするハックに近い
GitHub PagesでBrotliを使えない問題
- ページ読み込み時間を短縮する際は、HTML minifyよりHTTP圧縮のほうが効果が大きい
- HTTPは
Content-EncodingヘッダでgzipとBrotliをサポートする- gzipは低コストなので一般的にデフォルトで有効化される
- Brotliは通常gzipより圧縮率が良いが、はるかに遅い
- GitHub PagesはBrotliをサポートしていないため、サイトで最も長い記事
Recovering garbled Bitcoin addressesは、Brotliなら37 KiBで済むところがgzipでは92 KiBになる - この差のせいで読み込み時間が不要に2.5倍になる
最初に思いついた代替案と行き詰まった点
- GitHubが事前圧縮したBrotliファイルのアップロードと配信をサポートすれば問題を回避できるが、その機能は提供されていない
- クライアント側でJavaScriptにより直接展開する案は、WASMデコーダのサイズのため利点が薄れる
- brotli-dec-wasmは約
200 KB - tiny-brotli-dec-wasmは
71 KiB - 比較対象がgzip
92 KiBとBrotli本文37 KiB + 71 KiBになり、得がなくなる
- brotli-dec-wasmは約
- ブラウザのHTTPスタックにはBrotliデコーダがあるが、Compression Streams APIのDecompressionStreamは
gzip、deflate、deflate-rawしか受け付けない gzipをZopfliで事前圧縮しても86 KiBで、なおBrotliより大きい
画像フォーマットを圧縮コンテナのように使う
- ブラウザはすでに画像をデコードできるため、データを画像ピクセルに入れてCanvas APIで読み戻せば、展開ロジックを新たに実装しなくてよい
- GIFはrow-major順でデータを並べてからLZWを適用するが、gzipのDEFLATEはLZWを置き換えるために設計された方式なので、有利さは期待しにくい
- PNGはDEFLATEを使うが、ピクセル原本ではなく隣接ピクセルとの差分を圧縮する予測変換を先に適用する
- 例:
[a, b, c, d]の代わりに[a, b-a, c-b, d-c]を圧縮 - 予測値と実値の差が小さいほどHuffman圧縮に有利
- 例:
- 実験の核心は、可逆WebPであるVP8Lを汎用バイトデータ圧縮に活用すること
VP8Lがgzipと異なる点
- WebPには非可逆版と可逆版があり、ここでは可逆フォーマットのVP8Lのみを扱う
- VP8LはPNGのように予測変換を使うが、DEFLATEの代わりにGoogle製のDEFLATE類似方式を使う
- DEFLATEはファイルを複数チャンクに分け、それぞれに適したHuffman木を持たせられる
- JavaScript、SVG、マークアップが1つのHTMLに混在する場合、異なる木のほうが有利なことがある
- VP8Lは任意に大きなHuffman木テーブルを定義し、各16x16ピクセルブロックごとに異なる木を使える
- JavaScriptの次にCSS、その後またJavaScriptが来る場合、DEFLATEでは似た木を何度もエンコードする可能性がある
- VP8Lは木の再利用が可能で、より頻繁に、より低コストで木を切り替えられる
- VP8Lのcolor cacheでは、特定の性質を持つ直近のピクセルをコピーせよ、という形で値を短く表現できる
最初のWebP圧縮実験
- テストファイルは
Recovering garbled Bitcoin addressesのHTML- 元サイズ:
439,478バイト gzip --best:94,683バイト
- 元サイズ:
- Rustのwebp crateでバイト列をgrayscale RGB画像に変換し、可逆WebPとして圧縮した
- grayscaleを使った理由は、WebPの
subtract green変換のため- grayscaleではGチャンネルをR/Bから引くと、R/Bは実質0になる
- WebPは3チャンネルを個別のHuffman木でエンコードするため、固定値チャンネルは
O(1)空間に近くなる
- 当初は
1xN画像にしようとしたが、WebPは最大16383x16383までしかサポートせず、VP8_ENC_ERROR_BAD_DIMENSIONエラーが発生した 16383xNの形に合わせると結果は45,604バイトとなり、gzipの2倍小さく、bzip2の49,764バイトよりも小さかった
WebP向けの調整
- 幅広い画像でrow-major順を使うと、16x16ブロック内に入力上で離れたバイトが混ざってしまう
- 画像形状を縦長の
27x16383に変えると、圧縮結果は43,232バイトまで減った cwebpの圧縮性能設定であるmethodを0~6まで比較した- method 0:
48,902 - method 1:
43,546 - method 2:
43,442 - method 3:
43,292 - method 4:
43,232 - method 5:
43,182 - method 6:
43,182
- method 0:
- method
5はmethod6と同じサイズを出しつつ、より高速な選択肢として採用した - この時点でWebPはgzipより2.2倍小さく、Brotliより1.2倍大きい
複数ファイルでのベンチマーク
- 比較対象はsnappy testdata、Canterbury CorpusとLarge Corpus、SVGファイル2件だった
- 比較フォーマットは
gzip --best、brotli --best、bzip2 --best、WebP圧縮スクリプト - WebPは、ごく小さいファイル
grammar.lsp、xargs.1と一部例外を除けば、ほぼ常にgzipより良かった - 例外は
kennedy.xlsとpaper-100k.pdfpaper-100k.pdfは19 KBのXMLの後ろに圧縮データがあり、実質的には小さなデータを測っている状況kennedy.xlsはBrotli/bzip2との相対性能も不自然で、近接位置に異質なデータが多く、圧縮器が扱いにくいファイルなのかもしれない
- WebPはbzip2よりやや劣る傾向だが、場合によっては上回る
- WebPは
fireworks.jpegのような、ほぼ均一なランダムblobに近い例外を除けば、常にBrotliより劣っていた - 大きなplain-textデータではgzipに対して測定可能な改善を示した
- SVGファイルでも改善があった
html_x_4ではWebPが3.3%の圧縮率を記録し、Brotliの2.8%には及ばないがgzipの13%より大幅に良かった
JavaScriptで復元する
- WebPのデコード自体は
fetch、createImageBitmap、OffscreenCanvas、getImageData、TextDecoderで実装できる - ピクセルのRチャンネルを元のHTMLバイトとして使い、これをUTF-8でデコードして
document.documentElement.innerHTMLに入れる構成 - Canvas APIはfingerprintingによく使われるため、一部ブラウザは
getImageDataの結果にノイズを加える- Firefoxのstrict tracking protectionでは1%未満のピクセルが影響を受ける可能性がある
- HTMLではこのノイズが誤字のように現れる
- WebGLの
readPixelsを使うと、当時はノイズなしで動作した- WebGLはテクスチャを安定して
2048x2048までしかサポートしないため、再びサイズ制約を合わせる必要がある - この復元コードはminify後で約
550バイトだった
- WebGLはテクスチャを安定して
- WebPとコードを合わせて
44 KiBとなり、gzip92 KiB、Brotli37 KiBと比較された - 2026年4月15日以降、Firefoxが
readPixelsにもanti-fingerprintingを追加したため、この方式はそのままではもはや動作しない
画面のちらつきとスクロールの問題
awaitはpromiseベースで処理されるため、WebPのダウンロードが終わる前にブラウザはスクリプト実行が終わったと判断する- DOMがまだ空なので、ユーザーは短時間真っ白な空画面を見ることになる
- 緩和策として、スタイルとページ上部の約
8 KiBはgzip HTMLに残し、ビューポート下のコンテンツだけをWebP圧縮できる - 再読み込み時のスクロール位置復元も問題になる
- たとえば
Y = 5000pxの位置で再読み込みしたのにページ高さが0pxだと、位置が初期化される - 非常に大きい一時
divを追加すると役に立つ
- たとえば
document.writeではなくdocument.documentElement.innerHTMLへ代入する必要があり、そうすることで現在の文書を新しい文書に置き換えず更新できる
WebPをJavaScriptに直接埋め込む
- 遅延を少しでも減らすため、WebPをJavaScript内に直接埋め込める
- 最も単純な方法はbase64 data URL
- base64は元サイズを
1.33倍に増やすが、gzipがこの増加分をほぼ相殺するcompressed.webpをbase64にすると57,576バイト- これをgzip
--bestで圧縮すると43,519バイト
- WebPのような圧縮済みblobはほぼ均一なランダムデータであり、base64の8ビット-to-6ビット変換結果に対してgzipのHuffman木がほぼ逆変換のように働く
- UnicodeやUTF-16を使うこともできるが、base64は十分な第一解として残る
実際の適用とその後の状態
- 記事執筆時点では、このページ自体が古いブラウザやJavaScript無効環境でない限り、「Fool me twice」セクション以降をWebPで圧縮していた
- ページのWebP画像は実際のコードでは縦長で細いが、見やすい正方形WebPの例も提供されている
- 画像の明るい上部と下部はテキストとコード、約1/5地点のハッチング領域は図、暗い領域の大半は図中のテキストだった
- 実際の削減幅は限定的だった
- 既存のgzipページ:
88 KiB - WebP適用後のgzipページ:
83 KiB - Brotli想定:
69 KiB
- 既存のgzipページ:
- 2026年4月15日以降、Firefox訪問者に壊れたコンテンツが表示されないよう、現在は非圧縮ページに戻されている
- Rustコード、コーパス、その他のファイルはGitHubで公開されている
1件のコメント
Hacker Newsの意見
レイテンシを無視すればそうかもしれないが、実際にはロード時間が 0.001% ほど増える程度に見える
サイズの増加分はラウンドトリップの遅延に比べれば意味がなく、55KiB転送量を減らして節約できる時間より、展開にかかる時間のほうが大きい可能性もある
面白い実験ではあるが、この場合ユーザー体験はむしろ悪くなる可能性が高く、速度はほぼ同じなのに互換性だけが下がりそう
問題は、TFAの筆者のように100KBを50KBに減らそうと努力する人もいる一方で、ローミングデータでレストランの営業時間だけを見たい私に、画像を何十MBも平然と送りつけるところもあるということ
リソースへの意識は存在するが、残念ながら非常に偏って広がっている
接続が切れればすべて失い、ダウンロード済みの部分だけでも読むことも不可能になる
通常の接続では差は意味を持たず、50KBが重要になるほど非常に遅いか不安定な接続では、この方式は確実に悪くなる。面白い実験だが、サイトには適用しないでほしい
Symbols-2048-em%20Nerd%20Font%20Complete.woff2ファイルがあるので、その差をほぼ帳消しにしてしまう2.5倍の差ではないだろうが、0.001%でもない
損失のあるネットワークでは差が出るかもしれないが、確信は持てない
readPixelsがなぜフィンガープリント対策の対象ではないのか分からない。ほとんど見えないタイポをページ全体に撒き散らすわけではないので、私には問題ないgzipされたHTMLにはスタイルとページ上部の約8KiBだけを置き、ビューポート下のコンテンツだけをWebPで圧縮するという部分を見て、記事が任意の文の途中で突然切れて空白ページが続いた理由が分かった
LibreWolf を使っているのでWebGLがオフになっており、WebGLが必要なランダムなWebゲームはChromiumで使っている。WebGLをオンにすると記事はちゃんと動作し、正直かなりすっきりした手法だと思う
WWWは 普通のHTML から始めて段階的に強化する形で、普遍的にアクセス可能であるべき
Webブラウザで Brotliを直接使うこと も可能ではあるが、当然制約がある
2022年のJS1024応募作 [1] がこの概念の最初のデモだと思っており、任意圧縮用の概念実証コードもある。残念ながら、本来の目的だったサイズコーディングには合わなかった
主要な制約は、事実上ASCII文字に限られることと、明白な理由でレンダリングスタックに非常に敏感なこと。今はFirefoxでは動作しないように見える
[1] https://js1024.fun/demos/2022/18/readme
[2] https://gist.github.com/lifthrasiir/1c7f9c5a421ad39c1af19a9c...
Brotliがもともと設計された WOFF2フォントファイル形式 を使うことしかできないが、これを活用するにはフォントファイル全体を作る必要がある
最近のブラウザは、信頼できないフォントファイルをシステムに直接入れるのは非常に危険なので、通常はOpenType Sanitizer(OTS)でフォントをサニタイズする。そのため、OTSが受け入れる程度には正常でありながら、望むバイト列を内部に含み、取り出せるWOFF2ファイルを作らなければならない
多くの失敗の末、ほぼ制約なく2バイト符号付き整数列としてエンコードされるグリフ幅、つまりadvanceに落ち着いたという発想が見事
Chromium側が長らく阻んでいたが、zstd も今やWebに入ってきている。ついにChromeに入ったので、あとはSafariが追随すればよい
Batch Compress(https://batchcompress.com/en)を作っていて、最近 WebP 対応を追加してから間もなくデフォルトに変更した
私の知る限り、すでにウェブ圧縮ツールの中で最小級の JPEG を作れていたが、WebP は JPEG の約 50% のサイズにしかならなかった。対応追加後すぐデフォルトに変えるのは簡単な判断だった
サイトの利用者はかなり多いので、WebP をデフォルトに変えた後に多少不満が出ると思っていたが、1か月ほど経った今、WebP 関連の問い合わせや不満は1件だけだった
今ではほぼすべてのツールとブラウザが WebP をサポートしているようだ。最近、WebP 画像のアップロードを正しく処理できず次のステップに進めないウェブサイトを1つ見ただけで、最近はほとんど問題なくサポートされている
JPEG をうまく圧縮・最適化すれば、WebP に大きく劣るはずはない
JPEG よりほぼ同じに見える WebP を作って、いつでもファイルサイズを減らすことはできるが、ほぼ同じに見える JPEG として再圧縮しても同じことだ
これはすべての非可逆圧縮コーデックの特性で、品質が上がるほどファイルサイズは指数関数的に大きくなるため、人々はほとんど見えないごく小さな品質低下だけでファイルサイズが大きく変わることにいつも驚く
WebP は暗部のディテールを壊すひどいデフォルト設定で悪名高かった
ソースを見ていて、doctype 宣言に空白が抜けているのに気づいた。現在の形は誤りで、空白が入るべきだ
DOCTYPEの空白省略は、さらに短くできる厳密に言えば有効な HTML ではないが、それでも標準モードのトリガーには成功する
参考: https://GitHub.com/kangax/html-minifier/pull/970 / https://HTML.spec.WHATWG.org/multipage/parsing.html#parse-er...
私も https://FreeSolitaire.win でそのトリックを使っている
できるだけ多く削る minifier の出力のようだ [0]
0: https://github.com/KTibow/KTibow/issues/3#issuecomment-23367...
このトリックを以前使ったことがある。不思議なことに何に使ったのかは覚えていないが、おそらく可能かどうかを見るためだったようで、ここにもコメントを残していた: https://gist.github.com/gasman/2560551?permalink_comment_id=...
かなり昔のプロトタイプも見つけたが、単なるテストだったようだ: https://retr0.id/stuff/bee_movie.webp.html
今ではアドオンではなく拡張機能と呼ばれるスクリプト注入のようなものだが、いずれにせよ、最初に「ゴミ」を渡して後ろに JS を付け、ページに戻すというアプローチは興味深い
私の中のセキュリティオタクとしては、コメントフォームのようなユーザー提供データがあると攻撃が可能になるのか気になる
誰かがコメントに入れるバイト列を見つけ、圧縮後に私のスクリプトより前に位置して実行される
scriptタグに変わるようにできるのではないかと思うWebP ロスレスが PNG に追加しているものの大半はエンコーディングではなくモデリングに関するものであり、この種のテキスト圧縮では WebP のエンコーディング部分だけを使うことになるからだ
Google Fonts を外しても、ページ読み込み時間は少し改善するはずだ。リモートサーバーから読み込まれ、追加のハンドシェイクが必要になるためだ
このページは少なくとも Sailfish OS ブラウザでは壊れる。次の段落の後に長い空白がある
“Alright, so we’re dealing with 92 KiB for gzip vs 37 + 71 KiB for Brotli. Umm…”
とはいえ、gzip と Brotli による HTML 圧縮のオーバーヘッドは、最近のウェブサイトが使う JS、画像、動画の量に比べれば何でもない
個人的にはこの形式があまり好きではない。画像を保存したら WebP として保存されると、ウェブブラウザ以外では対応しているところがなく、編集したり意味のある形で使ったりする前に変換しなければならない
単に余計な手順を強制されているように感じる
それでも対応がさらに増えるなら悪くないと思う。20年に1回くらい新しい形式が出る程度なら我慢できる
.webmは消えてもいい