本セッションの登壇者
セッション動画
Fastlyの奥一穂と申します。今日はHTTP/3時代の優先度制御についてお話したいと思います。
仕事ではFastlyでTLSやQUICなどのHTTPのプロトコルスタックの開発に携わっています。その傍ら、本日お話するRFC 8297(103 Early Hints)やRFC 9218(Extensible Priorities)の著者を務めています。

HTTPの高速化の歴史とHTTP/3
最初にHTTPの高速化の歴史について振り返ってみましょう。
- HTTP/1.1(1997) … 複数のTCP接続を同時に確立してたくさんのリクエストを同時に発行したり、1本の接続を複数のリクエストのために使い回したり(keep-alive)できるように
- HTTP/2 (2015) … リクエストの多重化、優先度制御、サーバプッシュなどの機能が導入
- HTTP/3 (2022) … 新しい暗号化トランスポート層プロトコルQUICを採用

HTTP/3で採用されたQUICは2016年から開発が始まり、2022年に標準化が完了しています。TLS over TCPと比較して、
- より高速な接続確立
- より高度なパケットロスリカバリ
- ヘッドオブラインブロッキングが発生しない高速なストリーム多重化
などの利点があります。
では、HTTP/2にもあった優先度制御とサーバプッシュはHTTP/3でどうなったのでしょうか。

HTTP/2の優先度制御
HTTP/2の優先度制御は、下図のように木構造を使ってストリームの優先度を表現し、クライアントからサーバに伝えていました。木構造は汎用的ですが、同時に複雑です。
この例はFirefoxの優先度ツリーです。

こちらはChromeが構築した優先度ツリーで、木というより蛇のような構造をしていて、ブラウザによって異なる優先度ツリーの作り方をしていることがわかります。

HTTP/2の優先度制御は非常に複雑な仕様のため、いくつかの問題点があります。
- サーバ側の正しい実装が難しく、変な挙動を取るサーバが多い
- Webブラウザ側にも優先度制御を正しく要求できないものがある
- Webブラウザによって木の使い方が異なるため、サーバ側で優先度をチューニングしにくい

HTTP/2のサーバプッシュ
サーバプッシュは、リンクを検出してからリクエストを発行するまでにかかる時間を短縮するための仕様です。通常はHTMLをロードしてから、HTML内のlink
タグおよびscript
タグを検出してCSSとJSをリクエストするという流れです。ここでHTMLと同時にCSS / JSをまとめてレスポンスしてしまえば速くなるというのがサーバプッシュのアイデアです。

しかし実際にサーバプッシュで速くなるかをGoogleがChromeで実験した結果、 遅くなっていることのほうが多いということがわかりました。

なぜサーバプッシュで遅くなることが多いかというと、
- ブラウザがキャッシュ済みのアセットをプッシュしてしまう
- 画像などの本来優先度が低いアセットをプッシュしてしまう
- 他のアセットをプッシュしている分、サーバがHTML / CSSを送信するタイミングが遅れてしまう
という問題点によるものです。

HTTP/3での変更
これらのHTTP/2の問題点を踏まえ、HTTP/3では優先度制御が見直され、サーバプッシュはブラウザ側では実装せずに代替技術に投資するということになりました。

Extensible Priorities
Extensible Prioritiesは新しい優先度制御手法で、HTTPのバージョンに関係なく使用可能ですが、実際にはHTTP/3から導入されています。具体的には、HTTPリクエストヘッダまたはHTTP/3のフレームのpriority
ヘッダで優先度を指定します。下記の例では、u
(urgency; 優先度)が5、i
(incremental; 逐次処理可能)を?1
(true)に設定しています。また、サーバがHTTPレスポンスヘッダで優先度を変更することも可能です。

使用例を見てみましょう。まずは、アプリのアップデートを配信するためのリクエストGET /upgrade.pkg
が優先度u=7
で存在しています。

ここに、HTMLのリクエストGET /
がデフォルトのu=3
で追加されます。

さらに、CSSやJSのリクエストがu=1
、画像がu=5
で追加されます。

その後、サーバ側がレスポンスヘッダで優先度を指定することで、一部の画像のリクエストの優先度をu=4
に変更することもできます。

Webブラウザでは、HTTP/3に対応しているChrome / Edge / FirefoxはいずれもExtensible Prioritiesに対応しています。サーバ側では、CDNは各社対応済みのはずで、OSSサーバの中ではFastlyが開発しているh2oは対応していますが、他のサーバの対応状況については把握していません。

Webブラウザが実際にどのようなリクエストを送っているのか、こちらに詳しくまとめられていますが、一部奇妙な挙動も見受けられます。たとえば、最初のHTMLリクエストは本来の仕様上はu=3
であるべきですが、u=0
またはu=1
(旧バージョンの仕様?)もあります。またi
を付けていないブラウザもありますが、それだとHTMLが巨大な場合、HTMLをロードし終わるまでCSSをリクエストせず、ページが表示されないことになってしまいます。

このような状況ですので、HTTP/2よりは状況が改善しているものの、理想的な優先度制御ができているとは言えない状況であり、今後の課題だと思います。

103 Early Hints
103 Early Hintsはサーバプッシュの代替となる技術です。HTTP/2のサーバプッシュはレスポンスそのものをサーバからプッシュしていましたが、103 Early Hintsではプリロードすべきアセットのリンクを送ります。具体的には、サーバはHTTPのインフォメーショナルレスポンス(1xx)を返し、ブラウザはリンクを受け取ると、アセットをキャッシュしていなければプリロードします。

具体的なフローをみてみましょう。従来はHTMLを受け取ってからCSSをリクエストしていました。

103 Early Hintsを使うと、HTMLを生成している間にサーバからCSSへのリンクをブラウザに返し、ブラウザはHTMLを待たずにCSSのプリロードを開始できます。

先ほどの例のように、103 Early Hintsが最も効果を発揮するのは、WebページのHTMLがキャッシュ不可能で、かつエッジ - オリジンが遠い、またはWebページ生成に時間がかかる場合です。このような場合に、HTMLを生成/取得している間にCSSへのリンクをブラウザに送り、プリロードさせておくことでページ表示速度への効果を発揮します。

実際に103 Early Hintsを使用する方法をご紹介します。サーバ/CDN側については、たとえばFastlyであれば、このようなVCLを書くことで103 Early Hintsを有効にできます。

ブラウザ側ではChromeがすでに103 Early Hintsに対応しており、 link
ヘッダを認識します。link: <URL>; rel=preload
ヘッダを受け取ると、指定されたURL(同一オリジンのみ)をプリロードします。また、link: <URL>; rel=preconnect
ヘッダを受け取ると、接続のみ先に確立しておき、必要になったときにすぐにリクエストを送信できる状態になります。

103 Early Hintsを使用した場合の実際の効果について、Shopifyが公開した事例を紹介します。この事例では、プリロードを使用することでLCP(Largest Contentful Paint; ページロード開始からビューポート内で最も大きな画像/テキストブロックが描画されるまでの時間)の中央値が500ms改善したと報告されています。赤のバーが103 Early Hintsなし、青がありですが、各地域/OSで改善が見られることがわかります。

またWIXの事例ではpreconnectを使用した場合の結果が報告されていますが、TTFB(Time to First Byte; リクエスト送信からレスポンスの最初の1バイトが到着するまでの時間)は改善したが、FCP(First Contentful Paint; ページロード開始から何らかのコンテンツが描画されるまでの時間)とLCPについては大差がなかったとされています。

まとめ
HTTP/3は新しい優先度制御技術であるExtensible Prioritiesを採用しており、現状ではHTTP/2並みの性能ではあるものの、今後さらに良くなる余地があり、各開発者ががんばる必要があります。
103 Early Hintsについては、HTMLを動的生成している場合にpreloadの効果が大きいことが報告されており、検討の価値が大きいと思います。

ご清聴ありがとうございました。