🚦 CloudflareのEarly Hints(103)を有効化した — トグルの場所とAPI、そして103がすぐ返らない仕様

HTTP 103 Early HintsをCloudflareで有効化する手順。トグルはSpeed > SettingsのContent Optimizationタブ(APIでも可)。有効化直後に103が返らない仕様(Linkヘッダー学習)とcurlでの確認方法をまとめる。

読む深さ

このサイトのパフォーマンスをChrome DevToolsで監査したとき、改善余地は実質ゼロだった。LCP 417ms、CLS 0.00。それでも1つだけ残っていた札がEarly Hintsで、有効化しようとしたらダッシュボードでトグルを見つけられず、APIで入れた。あとから分かったんですが、トグルはちゃんとある。それも含めて、探し回った記録と有効化後にハマった仕様を書いておく。

103は「本題の前のひとこと」

Early Hints(HTTP 103)は、サーバーが本レスポンスを組み立てている間に「先にこれを読み込んでおいて」というヒントだけを先行して返す中間レスポンスだ。ブラウザは103に載ったLinkヘッダーを見て、HTMLが届く前にフォントやCSSの取得を始められる。

クライアント → リクエスト
          ← HTTP/2 103 (link: フォントCSSをpreload)  ← 即座に返る
          ← HTTP/2 200 (本体HTML)                    ← 揃ってから返る

出番があるのは「本レスポンスまでの待ち時間」がある場合と、クロスオリジンのリソースを早く繋ぎたい場合。このサイトは静的でTTFBが30ms程度しかないので、伸びしろは小さい。それでも当時はフォントCSSをCDN(クロスオリジン)から引いていたので、その取得開始を前倒しできる分の理屈上の得はあった。

追記(2026-07-26): そのフォントCDN依存のほうが問題だと後で分かった。第三者オリジンのstylesheetはレンダーブロッキングなので、相手が落ちるときではなく「中途半端に遅い」ときに描画が丸ごと待たされる。遅延1.5秒を注入して測ると初回描画は212msから3,340msまで伸びた。結局フォントを同一オリジンへ自己ホストし、Early Hintsのpreload先もそちらへ差し替えている。Early Hintsは接続確立を前倒しするだけで、レンダーブロッキングそのものは消せない。

トグルの場所と、探すのを諦めた人向けのAPI

正解を先に書くと、トグルは Speed > Settings の「Content Optimization」タブにある。自分は旧ダッシュボードの記憶で「Speed直下のOptimizationページ」を探して見つけられず、無いと思い込んだ。新ダッシュボードはSpeed配下がSettings+タブ構成に再編されていて、目当ての設定が一段深くなっている。同じ迷い方をする人は多いと思う。

UIを探すより速いのが好みなら、ゾーン設定APIでも一発で入る(このエンドポイントはEarly Hintsの解説ページではなくZone Settings APIのリファレンス側にある)。

# 現在値の確認
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/early_hints" \
  -H "Authorization: Bearer $CF_API_TOKEN" | jq .result.value

# 有効化
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/early_hints" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"value":"on"}'

前提がひとつあって、CloudflareはあなたのレスポンスにあるLinkヘッダーを元に103を作る。つまり先にLink: rel=preloadを返しておく必要がある。Cloudflare Pagesなら _headers に書くだけだ。

/*
  Link: <https://cdn.example.com/font-400.css>; rel=preload; as=style; crossorigin

公式ドキュメントが挙げている制約も先に押さえておくと無駄足が減る。103が返るのはHTTP/2とHTTP/3のときだけ。対象URIは拡張子が .html .htm .php か拡張子なしのもので、ステータスは200・301・302に限られる。relpreloadpreconnect。さらに、本レスポンスのヘッダーが既に手元にある場合(キャッシュ済みなど)Cloudflareは103を送らないので、キャッシュヒット率が高いページほど103を見る機会は減る。全プラン(Free含む)で使える。

ハマりどころ: 有効化直後は103が返らない

有効化してすぐ curl -v で確認したら、103が出ない。設定は on なのに、だ。一瞬「またUIと実体がズレてるのか」と疑ったが、数回叩いたら出た。

公式の説明は「Cloudflareは非同期にLinkヘッダーを引き当て、キャッシュした103を本レスポンスに先行して返す」。非同期でキャッシュを作る以上、1リクエスト目には間に合わない——という理屈だと思う。実際、こちらの検証では2リクエスト目以降に出るようになった。有効化→即確認→出ない→数回叩くと出る、という順番になる。確認コマンドはこれだけ。

curl -sv --http2 -o /dev/null "https://upstream-dev.com/" 2>&1 | grep -E "^< HTTP|link"

うちの本番の出力はこうなっている。

< HTTP/2 103
< link: <...400.css>; as=style; crossorigin; rel=preload, <...700.css>; ...
< HTTP/2 200
< link: <...400.css>; rel=preload; as=style; crossorigin, ...

103が200より先に出ていれば動いている。なお103はHTTP/2以上で機能し、http:// の平文接続には出ない。ブラウザ側の活用はChromeが先行していて、対応状況は資産の種類(preload/preconnect)によっても違うので、「全ユーザーが恩恵を受ける」前提では見積もらないほうがいい。

設定ひとつに大袈裟な、と思うかもしれない。ただ「トグルが再編後のUIの一段深い場所にいる」「有効化しても即座には観測できない」の二段構えは、知らないと諦めるポイントとして絶妙すぎる。同じ場所で引き返した人がいたら、Content OptimizationタブかAPI、そして数回のリトライで通れます。TTFBが数百msあるオリジン(WordPressや重いSSR)ほど差が出る機能なので、静的サイトのうちより、あなたのサイトのほうが得をするはずです。

元ネタ: https://developers.cloudflare.com/cache/advanced-configuration/early-hints/