No products in the cart.
TTFBが長すぎる場合はどうすれば良いでしょうか?この記事では、実際の事例を用いて、CDNのキャッシュ戦略がファーストバイトタイムを効果的に短縮する方法を分析します。具体的には、キャッシュ構成、オリジンサーバーへのリクエスト削減、パフォーマンス最適化技術などを用いて、ウェブサイトの速度とユーザーエクスペリエンスの向上を図ります。
TTFB(Time To First Byte)とは、簡単に言うと、ブラウザからリクエストを送信してから、サーバーから最初の1バイトのデータが返ってくるまでの時間のことです。TTFBはユーザーが最初に感じる応答速度に直結するため、低いほど良いとされています。ユーザーがリンクをクリックしてから白い画面が表示されている間、ブラウザは「ぐるぐる」回っているように見えますが、この「白画面時間」の裏側でTTFBは重要な役割を果たしています。
ただし、ユーザーが体感する「白画面時間」は、実際にはTTFB+ダウンロード+レンダリングの組み合わせです。TTFBは白画面時間の大部分を占めるとはいえ、それだけが全てではありません。
1.最初のバイトが届くまでに長い遅延が発生する原因は何でしょうか?代表的な要因をいくつか挙げます。
DNSルックアップ – ユーザーのリカーシブDNSリゾルバーが遅い、または権威DNSサーバーが遅い場合、この処理に数十ミリ秒、場合によっては数百ミリ秒かかることがあります。(WebPageTestなどのツールではDNSの時間は別に表示されます。)
ハンドシェイク – HTTPSリクエストの場合、データ転送を始める前にTCPの3ウェイハンドシェイクとTLSネゴシエーションを完了する必要があります。コネクションを再利用すれば大幅に高速化しますが、初回アクセス時やコネクションプールが冷えているときは、かなりの時間がかかります。TLS証明書チェーンが正しく設定されていないと、余分なラウンドトリップが発生し、TTFBが200msも増える可能性があります。
CDNノードの処理時間 – リクエストが届くと、CDNノードはキャッシュを確認し、レスポンスヘッダを準備し、エッジコンピューティングを実行することもあります。通常CDNノードは1~5msですが、キャッシュルールが複雑だったりノードの負荷が高いと10~20msまで増えることもあります。
オリジンへのバック遅延 – キャッシュミスが発生すると、CDNノードはオリジンサーバーからフェッチします。オリジンが別の大陸にある場合、大陸間のラウンドトリップタイム(RTT)は150~300msが普通です。さらにオリジンでの処理(PHP、DBクエリ)が加わると、簡単に500ms以上になります。
要するに、TTFBが高いということは「キャッシュミスが多すぎる+ヒット率が低すぎる」ことを意味します。ユーザーはTTFBが高いと「オリジンが遅い」と即座に考えます。そのためデータベースを最適化したり、Redisを追加したり、PHPをアップグレードしたりします。これらも確かに効果はありますが、根本原因を取り除くわけではありません。もしほとんどのリクエストが依然としてオリジンまで戻っているなら、どんなに高速なオリジンでも、直接応答を返すCDNエッジノードには敵いません。適切なキャッシュヒット率があれば、TTFBはほぼ「ユーザー→エッジノードのネットワークRTT + CDN処理時間(通常50~150ms)」になります。オリジンの速度はもはやほとんど問題になりません。

2. テスト環境
テスト環境の詳細は以下の通りです。実際のキャッシュヒットとオリジンへのバックシナリオを模倣するために、YewSafeのエッジキャッシング手法をリファレンスアーキテクチャとして採用し、実運用のCDNキャッシュ設定を使用しました。
3. 現実検証:3つのCDNキャッシング手法の比較
3.1 キャッシュなし
設定: ドメインのキャッシュを完全にオフにするか、オリジンが Cache-Control: no-store, no-cache, private を返すようにしました。実際の設定ミスに近い後者のオプションを選択しました。
結果(東京ノード → シンガポールオリジン):
TTFB平均: 923ms
最小: 780ms(オフピーク時)
最大: 1,340ms(夕方のピーク時)
オリジンバック率: 100%
キャッシュヒット率: 0%
各リクエストは東京CDNノードからシンガポールのオリジンへ向かいます。ネットワークRTTは約90~110ms、さらに初回リクエストではTLSハンドシェイク(追加のラウンドトリップ)が発生。オリジンのPHP処理は180ms、DBクエリは50ms、転送時間が加わります。したがって900ms超は普通です。ユーザー体験としては、リンクをクリックしてから白画面にほぼ1秒も費やすことになります。
3.2 静的アセットのキャッシュ
設定: 多くのCDNがデフォルトで使用する方法です。/static/、/media/、/js/、/css/ などの静的ファイルには非常に長いキャッシュ時間(max-age=31536000)を設定し、HTMLは Cache-Control: no-cache を維持します。そのためHTMLのバックオリジンリクエストは毎回バリデーションのために必要です。
結果:
TTFB平均: 712ms
最小: 610ms
最大: 1,030ms
オリジンバック率: ~65%(HTMLは常に、静的ファイルは状況による)
静的アセットヒット率: ~88%
HTMLヒット率: 0%
静的アセットのキャッシュは確かに正しい方向への一歩ですが、この場合のTTFBの主な制限は常にHTMLのバックオリジンです。最初のバイトのTTFBはHTMLレスポンスヘッダだけで決まるからです。HTMLがオリジンから取得される限り、TTFBは高いままです。
3.3 フルページキャッシュ
設定: HTMLもキャッシュするようにしました。警告: 動的コンテンツを誤ってキャッシュすると、他のユーザーのカートが見えてしまう可能性があります。キャッシュキーには「URL+一部のCookie」を使用し、適切なTTLを設定しました。
設定内容:
Cache-Control: public, max-age=300, stale-while-revalidate=60
CDNエッジTTL: 600秒
キャッシュキーに含めるもの:完全なURL(クエリパラメータ含む)、通貨Cookie、ストアCookie。utm_* と PHPSESSID は無視。
結果:
TTFB平均: 287ms
最小: 142ms(東京ノードキャッシュヒット時)
最大: 510ms(キャッシュ有効期限切れ後の最初のリクエスト)
オリジンバック率: ~18%
キャッシュヒット率: ~82%
比較表(直近10リクエストの平均):
| 戦略 | 平均TTFB | P95 TTFB | オリジンバック率 | ヒット率 |
|---|---|---|---|---|
| キャッシュなし | 923ms | 1,250ms | 100% | 0% |
| 静的アセットキャッシュ | 712ms | 980ms | 65% | 0% (HTML) |
| フルページキャッシュ | 287ms | 410ms | 18% | 82% |
フルページキャッシュによりTTFBは約70%削減されました。また、P95が1,250msから410msに低下しており、テールレイテンシがさらに改善されています。キャッシュが有効な場合、CDNエッジノードはほぼ即座にレスポンスを返せるようになり、大陸間のネットワーク変動の影響を受けなくなります。
さらに、ライブテストにおいてCDN間のキャッシュ柔軟性の品質レベルも大きく異なります。一部のCDNではグローバルTTLのみ許可され、パスベースのルールやカスタムキャッシュキーが使えません。YewSafeのようにパスレベルのルールやカスタムキャッシュキーをサポートするソリューションは、オリジントラフィックの削減とTTFBの安定化において大きなアドバンテージを発揮します。

4. CDNキャッシュがどのようにTTFBを劇的に削減するか
TTFBが高いと、つい「バックエンドが悪い」と決めつけてしまいがちです。しかし考えてみてください。あなたのオリジンサーバーはシリコンバレーにあり、ユーザーは東京にいます。何をどう頑張っても、光の速度だけでも往復150~200msかかります。TCPの3ウェイハンドシェイクとTLS鍵交換を加えると、レイテンシの大きい回線では各ラウンドトリップが高コストになります。そのような状況では、オリジンのCPU使用率が5%なのに、ユーザーはすでに半秒も待たされているということは珍しくありません。
ではCDNキャッシュは何をするのでしょうか?それは本質的に「エッジノードのストレージ容量をユーザーの時間に変換する」ことです。具体的に言うと:
物理的な距離を縮める – コンテンツをユーザーの都市にあるエッジノードに転送します。ユーザーはリクエストを海の向こう側に送る必要がなくなり、近くのノードからサービスを受けられます。ゲームのダウンロードクライアントで、これだけでTTFBが100ms以上削減されたのを観察したことがあります。物理法則には勝てませんが、距離を迂回することはできます。
ハンドシェイクの往復をなくす – 高レイテンシネットワーク上でのHTTPSハンドシェイクは複数回のラウンドトリップを必要とします。近くのCDNノードはハンドシェイクをほぼ瞬時に完了します。さらにCDNはオリジンに対して持続的な接続を常に維持しています – あたかも事前に敷設されたパイプのようなものです。そのため、キャッシュミスでオリジンにフェッチしに行く必要がある場合でも、新しい接続を構築する代わりに既にできあがったパイプを介して提供されます。
バックエンドロジックをオフにする – 多くのAPIエンドポイント(製品設定の取得、お知らせリストなど)は、必ずしもリアルタイムの計算を必要としません。それらをエッジでキャッシュする – たとえ数秒間だけでも – すると、データベースやコードに依存していたリクエストがエッジのメモリから直接処理されます。TTFBは数十ミリ秒まで低下します。
これは、階下にコンビニがあるようなものです。水を買うたびにわざわざ工場まで行きません。道のりを数キロメートルから数メートルに縮めれば、TTFBを50%以上削減することは十分に可能です。
5. 実績のある最適化戦略
多くのプロジェクトに裏付けられた、パフォーマンス最適化の海域におけるいくつかの救命ボートをご紹介します。
5.1 Cache-Control をシンプルに
Cache-Control はCDNの動作を決める指令です。オリジンは常にそれを送信し、CDNは通常それに従います(オーバーライドしない限り。ただし推奨されません)。
静的アセット(JS/CSS/フォント):Cache-Control: public, max-age=31536000, immutable。1年間を推奨。immutable はブラウザとCDNの両方に「このリソースは絶対に変更されません」と宣言します。更新するときは単にファイル名のハッシュを変更すればOKです。
HTML(個人化されていないユーザー体験):Cache-Control: public, max-age=300, stale-while-revalidate=60。5分間は新鮮な状態、その後60秒間は古いコンテンツを提供しながら非同期で再検証できます。これによりキャッシュ有効期限切れ時のスタンピードを防ぎます。
API呼び出し(キャッシュ可能な読み取り専用エンドポイント):Cache-Control: public, s-maxage=60, max-age=0。s-maxage はCDNなどの共有キャッシュのみに適用されます。max-age=0 はブラウザにキャッシュしないよう指示します。CDNは60秒間キャッシュしますが、ブラウザは常にCDNに確認します(CDNが持っていればそれを返し、なければオリジンへ行きます)。
個人化されたコンテンツ(ログイン後のホームページ、カートAPI):Cache-Control: private, no-store。キャッシュは一切しません。CDNは必ずオリジンに通過させます。どうしてもキャッシュする必要がある場合は、ユーザーIDをキャッシュキーに含めてください。
5.2 パスベースのキャッシング
同じドメイン内のURL(パス)が異なれば、更新頻度やコンテンツの個人化の程度も異なるため、異なる扱いが必要です。こうした状況では、CDNの管理コンソールでキャッシュルールを設定するのが最も一般的です。
| パスパターン | エッジTTL | Cache-Control | 備考 |
|---|---|---|---|
| /static/* | 1年 | public, max-age=31536000, immutable | ビルド成果物 |
| /media/catalog/product/* | 7日 | public, max-age=604800 | 商品画像、ほとんど変更なし |
| /product/* | 1時間 | public, s-maxage=3600, max-age=0 | 商品詳細、変更頻度は中 |
| /category/* | 5分 | public, max-age=300, stale-while-revalidate=60 | カテゴリ一覧、変更やや速い |
| /customer/* | なし | private, no-store | ユーザーセンター、リアルタイム必須 |
| /api/v1/prices | 10秒 | public, s-maxage=10 | 価格エンドポイント、高鮮度必要 |
このようなきめ細かい制御によって、パフォーマンスとキャッシュミスのリスクのバランスを取ることができます。
5.3 キャッシュキーの最適化
CDNが2つのリクエストが同一かどうかを判断する際に使うのがキャッシュキーです(コンテンツを一意に識別する文字列)。デフォルトでは、完全なURL(クエリパラメータ含む)といくつかのヘッダの組み合わせがキーになります。しかし、URLには不要なパラメータが多数含まれていることがよくあり、それらがキャッシュキーに含まれると、同じコンテンツが無数にキャッシュされてしまい、ヒット率が急落します。
例えば、あるサイトでは、ホームページのURLにトラッキングパラメータ(例:?utm_source=facebook&utm_campaign=summer)が付いていることに何ヶ月も気づきませんでした。キャンペーンごとに異なるため、ホームページは何千もの個別コピーとしてキャッシュされ、ヒット率は10%未満、TTFBはしばしば800msのバックオリジン状態でした。
修正方法: レスポンスに影響しないパラメータをCDNで無視するよう設定します。
無視するパラメータ:utm_source, utm_medium, utm_campaign, utm_term, utm_contentfbclid, gclid, msclkidref, referrer_ga, _gl
各種A/Bテストパラメータ(テストバージョンによって実際にコンテンツが変わる場合を除く)
保持するパラメータ:id, sku, product_idpage, offset, limit(ページング)sort, order(並び順を変える場合)currency, store(マルチカレンシー/マルチストアシナリオ)
クエリパラメータに加えて、Cookieもキャッシュキーの汚染を引き起こす可能性があります。一部のCDNはデフォルトで Cookie ヘッダ全体を含めます。つまり、PHPSESSID が異なるユーザーごとに別々のキャッシュコピーが作られてしまいます。正しいアプローチは、レスポンスに影響するCookie(currency、store_code など)のみを含め、PHPSESSID や _ga などは無視することです。
このようにキャッシュキーの設計がヒット率に直結します。YewSafeは高度なキャッシュ制御(柔軟なキャッシュキールール、特定のクエリパラメータの無視、ヘッダのカスタマイズ、Cookieフィールドの選択的包含)をサポートしており、動的パラメータがキャッシュを無効化するのを防ぐ主要な機能の一つとなっています。
5.4 エッジキャッシュTTLの設定
TTL(Time To Live)とは、キャッシュが有効な期間のことです。短すぎるとバックオリジンが増え、長すぎると古いコンテンツを表示します。
私の経験則(ビジネスに合わせて調整してください):
| リソースタイプ | 推奨TTL | 理由 |
|---|---|---|
| ハッシュ付き静的アセット(JS/CSS/フォント) | 1年 | コンテンツは決して変わらない。ファイル名で更新 |
| ユーザーアバター、汎用画像 | 1ヶ月 | 変更頻度低、古くなっても影響小 |
| 商品画像、ブランドバナー | 7日 | 更新頻度中 |
| ブログ記事コンテンツ | 1日 | 毎日更新されない場合 |
| 商品詳細ページ | 1時間 | 価格/在庫の変更は中程度 |
| ホームページ、カテゴリページ | 5分 | 集約コンテンツの変更は速い |
| リアルタイムデータAPI(在庫、価格) | 1分以内 | 高い鮮度が必要 |
また stale-while-revalidate を活用しましょう。例えば max-age=300, stale-while-revalidate=60 は、キャッシュ有効期限切れ後も60秒間は古いコンテンツを提供し、その間に非同期で新しいコンテンツを取得します。ユーザーはバックオリジンを待つことなく、TTFBは安定します。
5.5 キャッシュ事前ウォーミング
ライブページの公開時やキャッシュを手動でパージした直後は、最初のユーザーが高いTTFBのキャッシュミスを経験します。事前ウォーミングは、この問題を解決します。ユーザーがアクセスする前に、対象URLへのリクエストを事前に送信してキャッシュを埋めておき、ユーザーがすぐにエッジレスポンスを得られるようにします。
3つの方法:
デプロイ時の事前ウォーミング – コードやコンテンツの更新デプロイ後、CIスクリプトが自動的にCDN APIを呼び出し、重要なURL(ホームページ、主要商品ページなど)を事前ウォーミングします。デプロイ後、ユーザーはコールドスタートを経験しません。
スケジュール事前ウォーミング – トラフィックにはピークがあります(例:昼間に高く夜間に低い)。ピークの直前に人気ページを事前ウォーミングすると効果的です。
インテリジェント事前ウォーミング – ログを分析して今後ホットになるURL(キャンペーンページ、話題の商品など)を予測し、主要なエッジノードに事前格納します。データ駆動型で、手作業ではありません。

6. 高度な最適化
さらに深く掘り下げると、キャッシングの基本をマスターすればTTFBは200~300msまで下がっているはずです。そこから高度なテクニックを使って100ms以下に引き下げることも可能です。
6.1 リージョン別キャッシュ
すべての地域でコンテンツの鮮度の優先度が同じとは限りません。例えば、主要市場(北米、欧州)では鮮度を重視してTTFBを短くし、新興市場(南米、アフリカ)ではもともとのネットワークレイテンシが高いため、バックオリジンを減らすために長めのTTLを設定するのも良い選択です。このようなリージョンや国を条件としたルールをサポートするCDNは少数ですが存在します。
6.2 マルチCDNオーケストレーション
どの地域でも完璧なCDNは一つもありません。例えば、北米では非常に優れていても東南アジアではパケットロスが発生するものもあれば、欧州で強いが南米のカバレッジが薄いものもあります。
マルチCDNは2つ以上のCDNを組み合わせ、インテリジェントなDNSベースの解決やクライアントサイドのスピードテストによって、ユーザーをその時点で最適なCDNにルーティングします。これによりP95 TTFBを劇的に削減し、全体的な可用性を向上させることがよくあります。ただし、運用の複雑さ(設定同期、ログ集計、コスト配分)も増大します。
6.3 エッジ関数
CDNのエッジノードで軽量版のオリジンロジックを実行できれば、不必要なバックオリジンを減らせます。
例:
エッジリダイレクト – User-Agent からデバイス種別を判定し、エッジから直接302でモバイル版に転送します。
エッジ認証 – エッジでJWTトークンを検証し、無効なリクエストはオリジンに触れさせません。
エッジ集約 – 2つのバックエンドAPIの結果をエッジで結合し、クライアントのラウンドトリップを減らします。
エッジコンピューティングは、そうでなければオリジンに戻らなければならないリクエストのTTFBを下げるのに役立ちます。なぜなら、エッジノードは地理的にユーザーにより近いだけでなく、十分に強力かつ高速だからです。
7. 避けるべき落とし穴
これらは私自身が経験した失敗です。皆さんにはぜひ避けていただきたい。
動的ページの誤ったキャッシュ
初期の失敗として、「フルページキャッシュは素晴らしい、全部キャッシュしよう」と考えてしまいました。問題は、あるページにユーザーのニックネームとショッピングカートが含まれていたことです。サポートから「多くのユーザーが他の人のアカウントを閲覧しています」と30分後に連絡が来ました。パニックになってロールバックしました。現在では、ユーザー識別子を含むURLは Cache-Control: private, no-store にするか、どうしてもキャッシュする場合はキャッシュキーにユーザーIDを含めるようにしています。
TTLが長すぎる、または短すぎる
TTLが30秒のホームページを見たことがあります。これでは1日に約3,000回のバックオリジンが発生し、ヒット率は20%を下回り、TTFBは高いままです。逆に、価格エンドポイントで1時間のTTLを設定したため、「在庫切れ」の商品がまだ「在庫あり」と表示されていました。運用チームは激怒しました。ルール:TTLはビジネスが許容できる最大の古さに設定すべきです。自信がなければ短めから始めて、ヒット率を監視しながら徐々に増やしてください。
サンダリングハード(キャッシュスタンピード)
すべての商品ページのTTLが1時間で、同時に有効期限が切れると、次のリクエストの波はキャッシュを見つけられず、すべて同時にオリジンに戻ります。オリジンは負荷が急増し、応答が遅くなったり502エラーが発生したりします。対策:TTLにランダムなオフセットを追加する(例:3600~3900秒)、またはリクエストコラプシング(最初のリクエストだけがオリジンに戻り、他は待機)を使用します。
CDNの機能制限
安価なCDNの中には、パスベースのTTL、カスタムキャッシュキー、stale-while-revalidate を設定しようとすると、見た目は良くても壁にぶつかることがあります。きめ細かい計画が頓挫します。CDNを選ぶ際はテストを:クエリパラメータを無視できますか?パスごとにルールを設定できますか?リクエストコラプシングをサポートしていますか?これらの機能がTTFB最適化の上限を決めます。
TTFBの最適化は、多くの場合、「より速いオリジン」ではなく「より少ないオリジンリクエスト」を意味します。したがって、堅牢なCDNキャッシュ戦略(フルページキャッシュ、キャッシュキーチューニングなど)があれば、実際の環境でTTFBを30~50%簡単に削減できます。
FAQ
Q: TTFBはネットワーク時間ですか、それともサーバー時間ですか?
A: 両方です。キャッシュヒット時は主にネットワークRTT + CDN処理時間。キャッシュミス時はバックオリジンのネットワークとオリジン処理時間を含みます。
Q: 動的ページもキャッシュできますか?
A: はい。キャッシュキーを使って状態(通貨、ストアなど)を区別し、適切なTTLを設定してください。完全に個人化されたコンテンツ(例:ショッピングカート)はキャッシュすべきではありません。
Q: Cache-Control の s-maxage と max-age の違いは何ですか?
A: s-maxage はCDNのような共有キャッシュのみに適用されます。max-age はブラウザとCDNの両方に適用されます。よくあるパターンは、CDNには s-maxage を、ブラウザには max-age=0 を設定することです。
Q: CDNを使っているのにTTFBが高いままなのはなぜですか?
A: ヒット率を確認してください。低い場合、HTMLがキャッシュされていないか、キャッシュキーの設計が断片化を引き起こしている可能性があります。
Q: CDNキャッシュが機能しているかどうかをテストするにはどうすればよいですか?
A: curl -I https://example.com を使って、X-Cache: HIT や CF-Cache-Status: HIT(CDNによって異なります)といったヘッダを探してください。また、キャッシュあり/なしでTTFBを比較してください。
付録: すぐに使えるCDNキャッシュ設定
すぐに始められるように、オリジンのNginx設定とCDNルール設定のテンプレートを紹介します。ご自身のビジネスに合わせて調整してください。
オリジンNginx設定(/etc/nginx/sites-available/example.com):
server { listen 443 ssl http2; server_name example.com; # ハッシュ付き静的アセット location ~* \.(js|css|woff2?|ttf|eot|ico) { expires 1 y; add_header Cache-Control "public, immutable"; } # 画像 location ~* \.(jpg|jpeg|png|gif|webp|svg) { expires 30 d; add_header Cache-Control "public"; } # HTMLページ – 動的だがCDNキャッシュは許可 location / { add_header Cache-Control "public, max-age=300, stale-while-revalidate=60"; try_files $uri $uri/ /index.php?$args; } # APIエンドポイント – 短いキャッシュ location /api/ { add_header Cache-Control "public, s-maxage=60, max-age=0"; try_files $uri $uri/ /index.php?$args; } # ユーザーセンター – キャッシュなし location /customer/ { add_header Cache-Control "private, no-store"; try_files $uri $uri/ /index.php?$args; } # PHP処理 location ~ \.php { include fastcgi_params; fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }
CDNキャッシュルール推奨
| パス | エッジTTL | キャッシュキールール |
|---|---|---|
| /static/* | 31,536,000秒(1年) | クエリパラメータを無視 |
| /images/* | 604,800秒(7日) | トラッキングパラメータを無視 |
| /products/* | 3,600秒(1時間) | IDを保持、ソート/ページングは無視 |
| /api/* | 60秒 | 必須パラメータを保持 |
| / | 300秒 | 全てのクエリパラメータを無視 |
キャッシュキー最適化ルール:
無視するクエリパラメータ:utm_source, utm_medium, utm_campaign, utm_term, utm_content, fbclid, gclid, ref, _ga など。
保持するクエリパラメータ:id, slug, page, category – コンテンツを実質的に変えるもの。
キャッシュキーから除外:User-Agent(デバイス固有のコンテンツでない限り)。