103 Early Hintsで始める、 サーバー・フロントの協調最適化 1
Webページの表示には通信が必須 サーバーがHTMLを生成している間 描画やリソースの取得は止まる 2
Early Hintsが解決すること HTML生成中の待ち時間を使い、 CSS / JS / font などのリソース取得が開始 結果、画面描画が早くなる! HTMLのレンダリング中に、CSSの取得が並行して始まっている様子 3
HTML生成中の待ち時間を埋めるのが、 HTTP 103 Early Hints 4
このセッションのゴール バックエンドで どう実装するのか ブラウザが どう解釈・活用するのか どのUX指標が どれだけ改善するのか 5
プロフィール 発表者 ma@me 所属 最近の業務 品質改善・不具合対応 6
1xx のステータスコードの特徴 暫定レスポンスで、レスポンスボディを持たない レスポンスが2回返る ブラウザ Request 1xx 暫定レスポンス(ボディなし) Server 処理は継続 最終レスポンス(2xx など) 7
レスポンスが2回返っている HTML リクエストに 103 と 200 が並んでいる様子 8
Early Hintsのケース 通常の200ステータスのケース 9
103 応答ヘッダーの中身 約165バイトのヘッダーのみで、ボディよりも先に届く Link: 〜の部分が先行取得の指示 server: 103の送出箇所。このケースではFrankenPHPのCaddyから送出 10
Early Hintsを含む 歴代の先行取得手法を紹介 11
① HTTP/2 Server Push ブラウザで再現できないので、イメージ図 登場年代:2015 サーバーが要求されていないリソースをHTMLと同時に押し出す仕組み。 ブラウザのキャッシュ状況をサーバーが把握できず、不要なリソースまで送 ってしまうため廃止 12
② <link rel="preload"> <link rel="preload" href="/style.css" as="style"> <link rel="preload" href="/app.js" as="script"> 登場年代:2016 指定したリソースを優先的に先行取得するようブラウザに指示する HTML内で完結するため、サーバー側の対応が不要。主要ブラウザで広く対 応済み HTMLとは直列の取得になる 13
③ 103 Early Hints 登場年代:RFC 2017 / ブラウザ実装 2022 HTML生成中にリソースの先行取得を始めるため、HTMLとリソースの取得 が並列で進む。 14
各手法と取得タイミングまとめ 手法 タイミング 主導権 HTTP/2 Server Push レスポンス時 サーバー <link rel="preload"> HTML到達後 ブラウザ 103 Early Hints HTML生成中 サーバー 15
103は、本レスポンス到達前の空白時間を有効活用 ① <link rel="preload"> ― HTML到達後に発火 ③ 103 Early Hints ― 本レスポンス(200)到達前にヒント送信 16
103 Early Hints に対してサーバー側が どう主導権を握り どう扱うのか サーバー構成での実装を通して紹介 17
× Hono × Cloudflare PHP / Hono:バックエンドの処理を担当 Nginx / Cloudflare CDN:103 の生成・送出 18
構成の全体像 Client Node フロント Nginx Request サイドカー Request 内部Nginx + PHP Request ② 103 Early Hints ③ 200 OK(本文) 19
アプリは103を書かない 生成はインフラ / CDN 層任せ PHP + Nginx / サイドカー アプリは普通に200を返すだけ。103はサイドカー/Nginxが送 出 Hono + Cloudflare アプリは103を発行しない。CloudflareがエッジでLinkから103 を生成 20
103 を生成・転送する2つの部品 バックエンドの処理を待たず、サイドカーが res.writeEarlyHints() で 103 を生成。 それをフロントの Nginx が early_hints on; でクライアント へ転送する。 location / { early_hints on; # ★ upstream の 103 をクライアントへ転送 proxy_pass http://hints:3000; proxy_http_version 1.1; proxy_set_header Host $host; } 21
Early Hints が届く様子
curl -v --http1.1 -H 'Accept: text/html' http://localhost:8888/demo
< HTTP/1.1 103 Early Hints
← ① 先に届く(サイドカーが送出 / Nginx
< Link: </css/demo.css>; rel=preload; as=style
< HTTP/1.1 200 OK
< x-powered-by: PHP/8.4.21
← ② 約200ms後(PHPの本文)
← 200 は PHP-FPM 由来
ブラウザでの見え方
22
色々設定が大変だと感じた方 FrankenPHPなら headers_send(103); を差し込めば OK! https://frankenphp.dev/ 23
FrankenPHPって何?という方へ 過去のFrankenPHP関連の登壇スライド どこまで違う?!PHP実行環境パフォーマンス対決 - mod_php vs php-fpm vs Swoole vs FrankenPHP Node.jsに頼らずにFrankenPHPでリアルタイムWeb通信を実現する Laravel OctaneはFrankenPHPをどう高速化しているのか?ソースコードか ら読み解く、高速化の仕組み 24
実装ができても、どの画面でも効くわけで はない ここからは Early Hints が効く画面を見極め る話 25
103 が効く2つの画面構成 1. サーバー処理が長く、描画開始までの待ち時間が長い 2. クリティカルリソースが固定 26
1. サーバー処理が長く 描画開始までの待ち時間が長い画面 サーバー側の処理で、本レスポンスまでに時間がかかる(目 安:200ms〜) DB クエリ 外部 API 呼び出し など 27
イメージ コンテンツ例 EC のカート / 注文確認 ダッシュボード 検索結果 / 一覧画面 認証後のマイページ 28
2. クリティカルリソースが固定の画面 このページなら必ずこれを読む、リソースがバチっと決まって いる画面 29
LP イメージ コンテンツ例 LCP になるヒーロー画像 Web フォント ファーストビューの CSS エントリ JS 30
パフォーマンス検証 1. サーバー処理が長く、描画開始までの待ち時間が長い 2. クリティカルリソースが固定 実際にどれくらい速くなるのか、計測結果を見ていく。 31
FCP / LCP とは? FCP(First Contentful Paint) 最初のテキストや画像が表示された瞬間 LCP(Largest Contentful Paint) 主要コンテンツ(最大要素)が表示された瞬間 = ユーザーが体感する表示完了 32
計測対象のサイト 同じ HTML を返す2つのエンドポイントを用意。 共通条件 HTML を返すまでの遅延:200ms(両エンドポイント共通) = 200 OK までの TTFB:約 200ms 各アセット / API の遅延:各 200ms ※ TTFB は 103 の性質上、早く出てしまうため、参考数値扱い。最初のバイトが届いてもレンダリン グが始まらない。 33
計測対象のサイト 項目 /demo-no-early /demo-early 103 Early Hints の送信 なし あり( headers_send(103) ) preload 対象アセット — demo.css / inter-bold.woff2 / demo.js ダッシュボード 34
計測結果 ① サーバー処理が長く、描画開始までの待ち時間が長い画面 指標 なし あり 差 218 ms 10 ms −208 ms FCP 504 ms 308 ms −196 ms LCP 800 ms 384 ms −416 ms(−52%) 全リソース完了 693 ms 269 ms −424 ms TTFB ⚠️ 35
改善事項 LCP が 800ms → 384ms(−52%)に短縮 リソース読み込み遅延が 260ms → 7ms にほぼ消滅 LCP 計測時点でヒーロー画像はダウンロード完了済み 36
ネットワークタブ① 103なし HTML 完了後に CSS, JS が直列で開始。 font はさらに後(発見カスケード) 103あり HTML 生成中に CSS, JS, font, 画像を並行ダウンロード ※ ①(ダッシュボード)のネットワークタブ実測イメージ(数値は別計測) 37
計測対象 ② クリティカルリソースが固定の画面 38
計測結果② 全体指標 指標 なし あり 差 13 ms 9 ms −4 ms FCP 300 ms 308 ms +8 ms LCP 300 ms 308 ms +8 ms 全リソース完了 499 ms 228 ms −271 ms(−54%) TTFB ⚠️ サブリソース別(取得完了) リソース なし あり 差 CSS 441 ms 227 ms −214 ms JS 440 ms 228 ms −212 ms font 682 ms 228 ms −454 ms 画像 695 ms 267 ms −428 ms 39
改善事項 全リソース完了が 499ms → 228ms(−54%)に短縮 CSS → font の発見カスケードが解消し、並列ロードに LCP は元々高速(300ms 前後)で横ばい 40
ネットワークタブ② 103なし HTML後に CSS, JS が直列で開始 103対応 HTML生成中に CSS, JS, font, 画像を並行DL ※ ②(クリティカルリソース固定)のネットワークタブ実測イメージ(数値は別計測の中央値) 41
サーバー処理が長いほど効果あり HTML をクライアントへ返すまでの時間が長いページほど、 Early Hints の効果は大きい。 SQL クエリ 外部 API 呼び出し テンプレート描画 etc. 42
103で速度を解決! …とはいかないパターンも どんな構成にも効く銀の弾丸ではない。 効果が出るパターン、出ないパターンを確認。 43
フロント構成との相性問題 ✅ 効果あり MPA / SSR ❌ 効果薄 SPA(CSR中心) 44
なぜ SPA では効果が薄いのか サーバーが返すHTMLが薄く、待ち時間がほとんど生まれない ため、Early Hints の効果が薄い。 メインとなるHTMLのレスポンスタイム <!doctype html> <html lang="ja"> <head>...</head> <body> <div id="root"></div> <script type="module" src="/app.js"></script> </body> </html> ソース全体像 45
❌ とりあえずearly hints は 外したヒントは、かえって遅くなる 103は名前の通り、あくまでヒント。 そのページで使わないリソースを指すと逆効果 Link: </app.css>; rel=preload Link: </unused.css>; rel=preload app.cssは全てのページで利用 unused.cssはindexページでしか利用しないのに、全部のページ で読み込んでいる、など 46
適切に導入すれば ユーザー体験アップ ⤴️ ご清聴ありがとうございました 47