メインコンテンツへスキップ
ブログ一覧に戻る
Web制作・運用

TTFB(Time To First byte)完全ガイド:意味・測定方法・改善手法を網羅的に解説

2025年11月14日
最終更新: 2026年10月8日
12分で読めます

この記事でわかること

TTFBの測定区間をナビゲーション、応答ヘッダー、本文受信に分け、ブラウザ・Node.js・Python・curlでの確認方法を解説します。SSG・ISR、DB、接続プール、CDN、Redisの教材を通じ、キャッシュの安全な範囲、鮮度、同条件の比較、戻す条件まで実務の検証票に落とし込みます。架空値と実測を区別します。

2026年10月8日に、Lighthouse CIの現行設定資料とnode-redisの現行READMEを再読しました。Lighthouse CIは設定ファイルの優先順位、LHCI_接頭辞の環境変数、autorun・collect・assert・uploadの構成を案内しています。node-redisはRedis向けNode.jsクライアントとしてredisと@redis/clientの導入例を示します。これは設定とクライアントの仕様であり、実サイトのTTFB改善率、Redisの可用性、キャッシュ効果、運用費を証明するものではありません。

TTFB(Time To First Byte)は、通信の開始から最初の応答バイトを受け取るまでの時間です。ただし、ブラウザのナビゲーション全体、個別リクエストの待ち時間、サーバー内の処理時間では、測定の始点と終点が異なります。改善に着手する前に、どの区間を計っているかをそろえましょう。

30秒で要点

  • ブラウザのナビゲーションTTFBには、接続前の待機、リダイレクト、DNSやTLSなども関わります。DevToolsのWaitingだけと同じ値にはなりません。
  • TTFBの短縮だけでは、画像・JavaScript・描画を含む体感速度の改善を証明できません。LCPなどと同じページ・端末条件で追います。
  • 公開データのキャッシュ、DB待ち、接続の再利用を一つずつ検証します。個人情報を共有キャッシュへ入れてはいけません。

まず測定区間を言葉と式で固定する

web.devの定義では、ナビゲーション開始から最初の応答バイトまでを扱います。Navigation Timingを使う場合の基本的な区別は次のとおりです。responseStartはEarly Hintsなどの中間応答の影響も受け得るため、最終応答ヘッダーの時点が必要なら、対応環境でfinalResponseHeadersStartも確認します。

ナビゲーションTTFB = responseStart - startTime
リクエスト送出後の待ち = responseStart - requestStart
本文の受信区間 = responseEnd - responseStart
これらはサーバー内部の処理時間だけを表す値ではない

TTFBはどこから、どこまで?。0 ms、ナビゲーション開始、接続前の待機も含む始点。120 ms、リクエスト送出、ここから待つ区間は別指標。320 ms、最初の応答、ヘッダー・先頭バイトの観測。800 ms、本文受信完了、TTFBの終点ではない。ナビゲーション TTFB: 320 − 0 = 320 ms。送出後の待ち: 320 − 120 = 200 ms。本文の受信: 800 − 320 = 480 ms
架空の0・120・320・800msを使い、TTFB、送出後の待ち、本文受信を区別する。

図の値は計算のための架空例です。ナビゲーション開始0ms、リクエスト送出120ms、先頭応答320ms、受信完了800msなら、順に320ms、200ms、480msです。320msを「サーバーが320ms処理した」と言い換えることはできません。

TTFBはCore Web Vitalsそのものではありません。web.devが示す概略の目安は良好が0.8秒以下、遅い領域が1.8秒超ですが、集計対象やページ構成を外した一律の合否基準にはしません。SSRでHTMLを多く作るとTTFBが増えても、その後の描画が早まる場合があります。

ブラウザ・コマンド・実利用を混同せず測る

DevToolsで同じ条件を残す

Networkパネルで対象のdocumentを選び、キャッシュ利用、接続の再利用、リダイレクト、Timingの各区間を記録します。初回アクセスと再訪は別に集計します。次は実測値ではなく、記録書式の例です。

対象: https://example.com/catalog
条件: 初回接続 / 未ログイン / キャッシュ無効 / 同じ拠点
ナビゲーション開始: 0 ms
requestStart: 120 ms
responseStart: 320 ms
responseEnd: 800 ms
Waiting相当の差分: 200 ms(開始からの320 msとは別)

Lighthouseは診断と描画指標の確認に使う

LighthouseのPerformanceスコアは複数指標から計算され、TTFBの直接の代用品にはなりません。公式のスコア説明も実行環境による変動を説明しています。ローカルに固定バージョンのCLIを用意した環境では、許可したテストURLに対して次のようにレポートを残せます。ここに示すコマンドは外部サイトへ実行した実績を意味しません。

lighthouse https://example.com/catalog --only-categories=performance --output=json --output-path=./lighthouse-report.json

Node.jsではresponseイベントとendイベントを分ける

次の関数はNode.jsのHTTPSクライアントで、呼び出しから応答ヘッダーを受け取るまでを計ります。正確な先頭バイト到着そのものではなく、ヘッダー解析・コールバック実行を含む観測です。本文を消費して接続を解放し、タイムアウトでは明示的に破棄します。リダイレクトの自動追跡はしません。Node.jsのHTTP APIで各イベントの意味を確認できます。

async function measureHeaders(url, { get, now }) {
  return new Promise((resolve, reject) => {
    const started = now();
    const req = get(url, (res) => {
      const headersMs = now() - started;
      res.on('error', reject);
      res.on('end', () => resolve({ status: res.statusCode, headersMs, totalMs: now() - started }));
      res.resume();
    });
    req.on('error', reject);
    req.setTimeout(5000, () => req.destroy(new Error('timeout')));
  });
}
// Node.jsでの依存: get = https.get、now = performance.now

PythonのRequestsではelapsedがリクエスト送信から応答ヘッダー解析までの区間です。stream=Trueでも本文取得完了時間にはなりません。リダイレクトを切り、応答を閉じる例です。タイムアウトは処理全体の厳密な上限とは異なります。Requestsの仕様を基準に解釈します。

def measure_headers(url, session):
    with session.get(url, stream=True, allow_redirects=False,
                     timeout=(3, 5)) as response:
        return {
            "status": response.status_code,
            "headers_ms": response.elapsed.total_seconds() * 1000,
        }

curlのtime_starttransferは処理開始から最初のバイトまでの累積秒です。time_totalと区別し、接続とTLSの累積時刻も一緒に残します。シェルのtimeをTTFBと呼ばないことが大切です。次はPOSIXシェルの例で、Windowsでは破棄先などを環境に合わせます。curlの項目定義に従って比較します。

curl --silent --show-error --output /dev/null --write-out 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} first=%{time_starttransfer} total=%{time_total}\n' https://example.com/catalog

RUMでは母集団を残す

Pages Routerの例では、useReportWebVitalsから受けた指標を収集処理へ渡します。下のログは動作確認用で、分析基盤への送信実装ではありません。TTFBが届いたナビゲーションとクライアント内の画面遷移を同じ観測として水増しせず、端末・回線・地域・キャッシュ状態を許可された範囲で分けます。個人情報やURL内の秘密をログへ出さない設計が必要です。

import { useReportWebVitals } from 'next/web-vitals';
function reportMetric(metric) {
  if (metric.name === 'TTFB') {
    console.log({ name: metric.name, value: metric.value, id: metric.id });
  }
}
export default function App({ Component, pageProps }) {
  useReportWebVitals(reportMetric);
  return <Component {...pageProps} />;
}

Next.jsの計測ガイドと、全体像を扱うWebパフォーマンス入門を併用してください。

改善は遅い区間とデータの性質から選ぶ

表が収まらない場合は、横にスクロールできます

観測先に確かめること検証する変更戻す条件
初回接続だけ遅いDNS・TLS・接続先の距離接続先削減、配信拠点再訪が悪化、到達性低下
DB待ちが大きいクエリ回数・実行計画・待機索引、取得列、接続プール更新負荷やロック増加
公開ページで同じ計算を反復鮮度の許容時間・キャッシュキーSSG、ISR、公開結果キャッシュ古い価格や非公開情報を配信
TTFBは短いが表示が遅い画像・JS・CSS・描画転送量、実行量の削減見た目や操作の欠落

毎回計算する必要があるか

利用者ごとの結果と全員共通の公開結果を、先に分類します。以下は方式を選ぶ純粋関数で、Next.jsのキャッシュAPIではありません。getServerSidePropsと別ルーター向けAPIを混ぜる代わりに、要件を明示します。

function chooseRendering({ personal, maxStalenessSeconds, changesOnlyOnDeploy }) {
  if (personal) return 'request';
  if (changesOnlyOnDeploy) return 'static-build';
  if (maxStalenessSeconds > 0) return 'regenerate-with-stale-window';
  return 'request';
}

HTTP/2・HTTP/3は処理の遅さを消さない

NginxのHTTP/2設定例です。証明書、モジュール、TLS/ALPN対応などは別途必要です。設定後は構文検査と実際のプロトコル交渉を確かめます。下だけでは起動用の完全設定にはなりません。

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;
    ssl_certificate /path/to/fullchain.pem;
    ssl_certificate_key /path/to/privkey.pem;
}

HTTP/2モジュールとHTTP/3モジュールは要件が異なります。HTTP/3や0-RTTを有効にすれば全要求が即座に速くなる、とは判断できません。DB処理や転送先までの距離が支配的なら、そこは別に残ります。

SSGとISRは更新の約束を先に決める

次はPages Routerの独立したページ例です。自作のreadPublicContentがビルド時に公開データだけを返す前提で、会員情報には使いません。

export async function getStaticProps() {
  const content = await readPublicContent();
  return { props: { content } };
}

一定の古さを許容する別ページでは、ISRの間隔を設定します。60秒ごとの定期実行ではなく、間隔経過後のアクセスを契機に再生成が進むため、古い内容を返す期間があります。再生成失敗時に前の成功版が残る点も運用に含めます。

export async function getStaticProps() {
  const products = await readPublicProducts();
  return { props: { products }, revalidate: 60 };
}

Pages RouterのISR仕様に従う例で、App Routerの設定へそのまま移植する手順ではありません。

エッジでは依存先までの往復を数える

標準Request/Responseを使う小さな公開情報ハンドラーです。どの配信基盤へ配置するかは別の設定で決まり、関数を書いただけでエッジにはなりません。DBが遠隔なら、利用者との距離を縮めてもDB往復が増えることがあります。

function publicStatus(request) {
  if (request.method !== 'GET') return new Response('Method not allowed', { status: 405 });
  return Response.json({ service: 'catalog', status: 'available' }, {
    headers: { 'Cache-Control': 'public, max-age=30' },
  });
}

DB・接続・キャッシュを安全な境界で改善する

索引と取得方法を確認する

次は検証用DBで試すSQLです。索引を増やすほどよいわけではなく、更新コストと保存容量も増えます。PostgreSQLのEXPLAINのcostは経過ミリ秒ではありません。実行計画の説明を読み、実際の行数や待機も別に調べます。

CREATE INDEX idx_users_email ON users(email);
EXPLAIN SELECT id, name FROM users WHERE email = 'reader@example.test';

投稿ごとに著者を取得するN+1の役割は、結合の例で比較できます。著者が削除されても投稿を落とさない要件ならLEFT JOINを選びます。全列取得を避けても、件数制限や順序が曖昧なら大きな負荷は残ります。

SELECT p.id, p.title, u.name AS author_name
FROM posts AS p
LEFT JOIN users AS u ON u.id = p.user_id
ORDER BY p.id
LIMIT 20;

接続は必ず返す

node-postgresのPoolをプロセス内で使い回し、必要な場合のみcheckoutします。プールの公式説明のとおり、返却漏れは待機を引き起こします。次はpoolを受け取る関数です。生成時のmaxは全プロセス数との積、DB側上限、他用途の接続枠を考えて決めます。

async function loadUser(pool, id) {
  const client = await pool.connect();
  try {
    return await client.query('SELECT id, name FROM users WHERE id = $1', [id]);
  } finally {
    client.release();
  }
}

CDNへ公開できる経路を限定する

共有キャッシュは「APIだから」一律に許可しません。次は認証・Cookieのない公開カタログだけを候補にする方針例です。実際にはオリジン応答、CDNのキー、地域・通貨・言語などの変動要因も確認します。

function cachePolicy({ path, authorization, cookie }) {
  if (authorization || cookie || path !== '/api/public-catalog') return 'private, no-store';
  return 'public, max-age=0, s-maxage=60, stale-while-revalidate=30';
}

stale-while-revalidateは古い応答を許す設定です。Cache-Controlの仕様を基準に、価格、在庫、個人情報などの適否を判断してください。

Redisは接続と障害時の扱いまで用意する

node-redisではクライアント作成後の明示的な接続とエラー処理が必要です。次の関数は接続済みclientを受け取り、公開データだけをTTL付きで保存します。キャッシュの読書き失敗はログへ渡し、DBの障害は隠しません。実運用では同時missの集中、失効通知、値のサイズも設計します。

async function cachedPublic(client, key, load, onCacheError) {
  try {
    const value = await client.get(key);
    if (value !== null) return JSON.parse(value);
  } catch (error) { onCacheError(error); }
  const data = await load();
  try { await client.set(key, JSON.stringify(data), { EX: 60 }); }
  catch (error) { onCacheError(error); }
  return data;
}

接続準備と本文転送も別々に扱う

DNSの先読み、接続の先行確立、次ページの取得は別の操作です。対象が増えるほど通信や接続資源を消費するため、実際に使う重要な接続先だけに限定します。

<link rel="dns-prefetch" href="https://static.example.test">
<link rel="preconnect" href="https://static.example.test" crossorigin>
<link rel="prefetch" href="/catalog/next-page">

圧縮は本文転送量を減らす方法であり、必ずTTFBを下げる方法ではありません。CPU時間との交換もあります。次はNginxのhttp/server文脈に置く構成例です。機密を含む応答の圧縮は攻撃条件も検討します。

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/javascript application/json;

Nginx gzip仕様と併せ、転送の残りは画像の最適化、実行と描画はJavaScript・CSSの最適化で確認します。

EC・企業・ブログで検証票を作る

EC:検索結果と価格の責任を分ける

カテゴリ一覧が遅いという仮説なら、条件と並び順に合う索引を検討します。次の索引は例であり、実行計画や選択度を確認せず本番へ追加しません。

CREATE INDEX idx_products_category_price ON products(category_id, price);
SELECT id, name, price FROM products
WHERE category_id = 3
ORDER BY price, id
LIMIT 20;

公開商品情報はキャッシュできても、利用者別割引や購入時の在庫確定は別です。上のcachedPublicを利用する例ではキーに言語を含め、公開済み商品の取得だけを渡します。

async function getCatalog(client, locale, readPublished, onError) {
  if (!['ja', 'en'].includes(locale)) throw new Error('unsupported locale');
  return cachedPublic(client, `catalog:v1:${locale}`, () => readPublished(locale), onError);
}

画像の長期保存は内容ハッシュ入りURLに限定します。同じURLの画像を上書きする運用では下の設定を使いません。これはNext.jsの独立した設定例です。

module.exports = {
  async headers() {
    return [{ source: '/versioned-assets/:path*', headers: [
      { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
    ] }];
  },
};

企業サイト:更新頻度を公開手順に結び付ける

会社案内のように公開操作で更新できる内容なら、Pages RouterのSSGでビルド時に読む方法があります。問い合わせの個人情報を同じデータ取得へ混ぜません。

export async function getStaticProps() {
  const company = await readApprovedCompanyProfile();
  return { props: { company } };
}

動的機能を分離する例は、公開営業日情報だけを返す関数です。公開可否の判定を通ったデータを入力し、問い合わせ送信・認証・DB更新をこの公開キャッシュへ載せない設計にします。

function publicOfficeInfo(request, publishedInfo) {
  if (request.method !== 'GET') return new Response('Method not allowed', { status: 405 });
  return Response.json(publishedInfo, { headers: { 'Cache-Control': 'public, max-age=60' } });
}

ブログ:再生成の失敗時も古い版を識別する

ブログ一覧のPages Router例です。更新間隔だけでなく、公開した記事が反映されたかを確認する担当を決めます。ISRページのCache-Controlを独自ヘッダーで重ねて上書きする前提にはしません。

export async function getStaticProps() {
  const posts = await readPublishedPosts();
  return { props: { posts }, revalidate: 300 };
}

表が収まらない場合は、横にスクロールできます

検証票の欄記入した架空例
対象公開カテゴリ一覧、未ログイン、同一地域
仮説DBの反復取得が初期HTMLを待たせている
変更公開一覧のみ60秒キャッシュ、購入処理は対象外
観測同条件のTTFB分布、LCP、エラー率、価格の鮮度
戻す条件非公開情報混入、更新の許容時間超過、エラー増加
判定改善が再現し鮮度を満たせば継続、未観測なら保留

監視はスコアの閾値だけで終えない

次は.lighthouserc.cjsというJavaScript設定の例です。YAMLとして保存しません。実行URLとChrome環境を固定してから使います。80点はチーム内の仮の警告値で、TTFBの合格値やSEO成果の保証ではありません。

module.exports = {
  ci: {
    collect: { url: ['http://127.0.0.1:3000/catalog'], numberOfRuns: 3 },
    assert: { assertions: { 'categories:performance': ['warn', { minScore: 0.8 }] } },
  },
};

Lighthouse CIの設定仕様に従い、ラボ検証と実利用の分布を別に保存します。教材の関数や計算をローカルで確かめても、実Nginx、CDN、DB、Redis、Next.js配備の性能を測ったことにはなりません。

まず一つのURLについて、始点・終点・条件・更新の許容時間・戻す条件を記入してください。サイト制作の前提はWebサイト制作入門、構成全体はWebサイト設計、実装選択はモダンWeb開発、基盤はサーバー入門へつなげられます。

よくある質問(FAQ)