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
これらはサーバー内部の処理時間だけを表す値ではない
図の値は計算のための架空例です。ナビゲーション開始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開発、基盤はサーバー入門へつなげられます。