
はじめに
現代の Web アプリケーションにおいて、パフォーマンスはユーザー体験を左右する重要な要素です。これまで、gzip、Brotli、Zstandard といった優れた圧縮技術が、サーバーからクライアントへのデータ転送量を削減し、Web サイトの高速化に貢献してきました。
しかし、SPA(シングルページアプリケーション)や API 駆動型のアーキテクチャが主流となる中で、新たな課題が浮上しています。それは、数十 KB 程度の JSON データや動的に読み込まれる UI コンポーネントといった、比較的小さな HTTP レスポンスにおいて、圧縮効率が上がりにくいという問題です。既存の圧縮アルゴリズムは、データ内にある程度の繰り返しパターンが存在しなければ、その効果を十分に発揮できないという特性があるためです。
この課題を解決すべく登場したのが、新しい Web 標準仕様「Compression Dictionary Transport」です。
この記事では、この Compression Dictionary Transport の PHP への実装について解説します。
発想の転換:「繰り返しパターン」を事前に共有する
Compression Dictionary Transport の核心的なアイデアは、非常にシンプルです。
圧縮したいデータの中に繰り返しパターンが少ないなら、サーバーとブラウザの間で、あらかじめ「共通の辞書」を共有しておけば良い。
この「辞書」とは、サイトで頻繁に使われる文字列、HTML タグの構造、JavaScript の定型コード、CSS のクラス名など、予想される繰り返しパターンを集めたファイルです。
これを事前に共有することで、たとえ小さなデータであっても、辞書内のパターンと一致する部分を効率的に参照へと置き換え、驚異的な圧縮率を実現します。
Compression Dictionary Transport の仕組み
Compression Dictionary Transport1 は現在、IETF で標準化が進められており、主要なブラウザでの実装も始まりつつあります。
その基本的なワークフローは、Brotli 実装と Zstandard 実装で共通しており、HTTP ヘッダを介したサーバーとクライアントの連携によって成り立っています。
主要なHTTPヘッダー
| ヘッダー | 役割 |
|---|---|
Accept-Encoding |
クライアントが辞書圧縮 (dcb, dcz など) をサポートしていることをサーバーに通知します。 |
Use-As-Dictionary |
サーバーが辞書そのものを配信する際に使用し、ブラウザにこれを辞書としてキャッシュするよう指示します。 |
Available-Dictionary |
クライアントがキャッシュ済みの辞書のハッシュ値をサーバーに通知し、利用可能であることを伝えます。 |
Content-Encoding |
サーバーがレスポンスを辞書圧縮したことを示します (dcb または dcz)。 |
Vary |
Accept-Encoding, Available-Dictionary を指定し、プロキシキャッシュなどが正しく動作するようにします。 |
基本的なワークフロー
- 辞書の配布: サーバーが
Use-As-Dictionaryヘッダーを付けて辞書リソースを配信する。 - 辞書の保存: ブラウザは辞書を保存し、その内容から SHA-256 ハッシュ値を計算する。
- 辞書の利用通知: 次回以降のリクエストで、ブラウザは
Available-Dictionaryヘッダーに計算したハッシュ値を含めて送信する。 - 辞書圧縮の実行: サーバーは
Available-Dictionaryヘッダーを受け取ると、自身が保持する辞書のハッシュ値と照合する。一致すれば、その辞書を使ってレスポンスを圧縮する。 - レスポンスの返却: サーバーは
Content-Encodingヘッダー(例:dcb,dcz)を付与して、辞書圧縮されたデータをクライアントに返す。 - データの伸張: レスポンスを受け取ったブラウザは、指定された共有辞書を使ってデータを正しく伸張(解凍)し、元のデータを復元する。
この仕組みは、特に以下のようなユースケースで絶大な効果を発揮します。
- SPA: アプリケーションのバージョンごとに JS バンドルから辞書を生成し、API レスポンスの圧縮に利用する。
- 頻繁に更新されるライブデータ: 共通するデータ構造を辞書として共有し、差分だけを効率的に転送する。
- マイクロサービス間のAPI通信: サービス間で共通の辞書を用意し、通信ペイロードを削減する。
Brotli による実装 (Dictionary-Compressed Brotli)
php-ext-brotli では、Content-Encoding: dcb として Compression Dictionary Transport を実装します。
DCB バイナリフォーマット
dcb で圧縮されたデータは、仕様に従ったフォーマットで構築されます。
+--------------+-------------------------------+------------------+ | Magic Header | Dictionary SHA-256 Hash | Compressed Data | | (4 bytes) | (32 bytes) | (variable) | +--------------+-------------------------------+------------------+ | 0xff444342 | SHA-256 of dictionary content | Brotli compressed| | | | with dictionary | +--------------+-------------------------------+------------------+
PHP での実装詳細 (php-ext-brotli)
実装は、PHP の出力バッファリング (ob_start) をフックする形で行われます。
1. 設定と有効化
php.ini または ini_set で辞書へのパスを指定し、ob_start でハンドラを登録します。
ini_set('brotli.output_compression_dictionary', '/path/to/your/shared.dict');
ob_start('ob_brotli_handler');
echo "Hello";
?>
2. C言語レベルのコアロジック
内部では、以下の処理が実行されます。
- エンコーディングの解析:
Accept-Encodingヘッダを解析し、dcbが指定されているかをビットフラグで管理する。 - 辞書の検証:
HTTP_AVAILABLE_DICTIONARYヘッダの値と、サーバー側で指定された辞書から計算した SHA-256 ハッシュ値を比較する。ハッシュが一致しない場合は、安全に通常の Brotli 圧縮 (br) にフォールバックする。 - Brotliエンジンへの辞書アタッチ: Brotli のエンコーダーに辞書をセットする。
- DCBフォーマットの構築: 圧縮後、レスポンスの先頭にマジックヘッダーと辞書ハッシュを追加して、最終的な
dcbフォーマットのデータを作成する。
zval *available = zend_hash_str_find(..., "HTTP_AVAILABLE_DICTIONARY", ...); if (available) { PHP_SHA256_CTX context; PHP_SHA256Init(&context); PHP_SHA256Update(&context, ...); PHP_SHA256Final(ctx->dict_digest, &context); zend_string *b64 = php_base64_encode(ctx->dict_digest, 32); if (memcmp(ZSTR_VAL(b64), Z_STRVAL_P(available) + 1, ...)) { BROTLI_G(compression_coding) &= ~PHP_BROTLI_ENCODING_DCB; } } if (dict) { ctx->dictionary = BrotliEncoderPrepareDictionary(BROTLI_SHARED_DICTIONARY_RAW, ZSTR_LEN(dict), ZSTR_VAL(dict), BROTLI_MAX_QUALITY, NULL, NULL, NULL); if (ctx->dictionary == NULL || !BrotliEncoderAttachPreparedDictionary(ctx->encoder, ctx->dictionary)) { return FAILURE; } }
このフォールバック機構により、Compression Dictionary Transport が利用できない環境でもアプリケーションは問題なく動作します。
Zstandard による実装 (Dictionary-Compressed ZStandard)
php-ext-zstd では、Content-Encoding: dcz として Compression Dictionary Transport を実装します。
DCZ バイナリフォーマット
dcz で圧縮されたデータは、仕様に従ったフォーマットで構築されます。
+--------------------+-------------------------------+------------------+ | Magic Header | Dictionary SHA-256 Hash | Compressed Data | | (8 bytes) | (32 bytes) | (variable) | +--------------------+-------------------------------+------------------+ | 0x5e2a4d1820000000 | SHA-256 of dictionary content | ZSTD compressed | | | | with dictionary | +--------------------+-------------------------------+------------------+
PHP での実装詳細 (php-ext-zstd)
こちらも ob_start を利用した出力ハンドラとして実装されています。
1. 設定と有効化
php.ini または ini_set で辞書へのパスを指定し、ob_start でハンドラを登録します。
ini_set('zstd.output_compression_dict', '/path/to/your/dictionary.txt');
ob_start('ob_zstd_handler');
echo "Hello";
?>
2. C言語レベルのコアロジック
基本的な流れは Brotli 実装と似ていますが、Zstandard ライブラリの機能を使って実装されています。
- エンコーディングの解析: 同様に
Accept-Encodingを解析しdczフラグを管理する。 - 辞書の検証:
HTTP_AVAILABLE_DICTIONARYヘッダとサーバー側辞書のハッシュ値を比較する検証ロジックも同様である。不一致の場合は通常のzstd圧縮にフォールバックする。 - Zstandardエンジンへの辞書適用:
ZSTD_createCDictで辞書オブジェクトを作成し、ZSTD_CCtx_refCDictで圧縮コンテキストにセットする。 - DCZフォーマットの構築: 圧縮ストリームの先頭に、仕様で定められた 8 バイトのマジックヘッダーと 32 バイトの辞書ハッシュを書き込む。
zval *available = zend_hash_str_find(..., "HTTP_AVAILABLE_DICTIONARY", ...); if (available) { PHP_SHA256_CTX context; PHP_SHA256Init(&context); PHP_SHA256Update(&context, ...); PHP_SHA256Final(ctx->dict_digest, &context); zend_string *b64 = php_base64_encode(ctx->dict_digest, 32); if (memcmp(ZSTR_VAL(b64), Z_STRVAL_P(available) + 1, ...)) { ZSTD_G(compression_coding) &= ~PHP_ZSTD_ENCODING_DCZ; } } if (dict) { ctx->cdict = ZSTD_createCDict(ZSTR_VAL(dict), ZSTR_LEN(dict), (int)level); if (!ctx->cdict) { return FAILURE; } ZSTD_CCtx_refCDict(ctx->cctx, ctx->cdict); }
テスト
compression-dictionary-transport-shop-demo というデモを使用してテストを行います。
1. ファイルの準備
まず、デモ用のファイルをいくつか変更・作成します。
public/index.html の末尾にある辞書のリンクを、PHP で配信するように変更します。
- - +
public/dictionary.php を以下の内容で作成します2。
$file = __DIR__ . '/items/shop.dict';
header('Use-As-Dictionary: match="/items/*"');
header('Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT');
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($file));
header('Accept-Ranges: bytes');
readfile($file);
次に、デモ用の辞書とコンテンツを配置します。
data/shop.dictをpublic/items/shop.dictにコピーする。data/*.htmlをpublic/items/*.phpとしてコピーする。
コピーした各 PHP ファイル(1f3bf.phpなど)の先頭に、設定を読み込むための include 文を追加します。
+