メインコンテンツへスキップ
kt-tech.blog

【設定・環境構築】Cloudflare Pages × Next.js のSSR応答を66倍高速化した全記録

設定・環境構築8分で読めます

この記事でわかること

  • 記事ページの TTFB 4.23秒 → 0.064秒までの内訳(どの施策がどれだけ効いたか)
  • API 呼び出しの並列化で 40〜50% 改善できるという実測
  • Cache-Control ヘッダーだけでは Pages の動的応答が CDN キャッシュされないという落とし穴
  • Cache Rule の具体的な設定手順と、RSC リクエストを除外すべき理由

Cloudflare Pages で Next.js(Edge Runtime)を運用していること。Cloudflare ダッシュボードで Cache Rules を設定できる権限が必要です(無料プランで可)。

📌 概要: Cloudflare Pages上のNext.js(Edge Runtime + SSR)で記事ページのTTFBが4秒超だった問題を、API並列化・レート制限撤廃・CDN Cache Rule設定の3施策で0.06秒(66倍高速化)まで改善した全記録。効果のあった施策・なかった施策を正直にまとめています。

はじめに

技術ブログをNotion APIをCMSとしてCloudflare Pages上のNext.js(Edge Runtime)で運用していたところ、記事詳細ページのTTFB(Time to First Byte)が4秒超という致命的な遅さだった。

SSGが使えないEdge Runtime環境で、どうやって高速化するか試行錯誤した記録をまとめる。

1. 改善前の状況

なぜ遅い?

Edge RuntimeではgenerateStaticParams()が使えず、全ページがSSR。記事詳細ページでは以下のAPI呼び出しが直列で走っていた:

  1. Notion API: 記事データ取得(Slug検索)
  2. Notion API: ブロック一覧取得(本文)
  3. Notion API: 記事一覧取得(サイドバー用)
  4. 外部サイト: OGPデータ取得(リンクカード用)

さらにNotion APIへのレート制限対策として、リクエスト間に350ms のsleepを入れていた。

改善前の計測結果

ページ TTFB(初回) TTFB(2回目)
TOP / 0.23s 0.07s
記事詳細 4.23s 2.03s
記事一覧 2.23s 1.05s
カテゴリ 1.50s 0.72s
About 0.36s -

記事詳細の4.23秒は完全にアウト。

2. 施策①: API呼び出しの並列化 — ✅ 効果大

getDetail()getList()Promise.all()で同時実行に変更。

Before

TypeScript
const blog = await getDetail(blogId);
const { contents } = await getList();

After

TypeScript
const [blog, { contents }] = await Promise.all([
  getDetail(blogId),
  getList(),
]);

同様にカテゴリ・タグページでも並列化:

TypeScript
const [{ contents }, category_show] = await Promise.all([
  getList(),
  getCategoryDetail(categoryId),
]);

効果

ページ Before After 改善率
記事詳細 4.23s 2.20s 48%改善
記事一覧 2.23s 1.21s 46%改善
カテゴリ 1.50s 0.85s 43%改善

3. 施策②: レート制限sleepの削除 — ✅ 効果あり

Edge Runtimeではリクエストごとに新しいWorkerインスタンスが起動する。つまりグローバル変数でのレート制限は意味がない。

TypeScript
// Before: 350msの無駄なsleep
let lastRequestTime = 0;
async function notionFetch(path, options) {
  const elapsed = Date.now() - lastRequestTime;
  if (elapsed < 350) {
    await new Promise(r => setTimeout(r, 350 - elapsed));
  }
  lastRequestTime = Date.now();
  // ...
}

// After: sleepなし
async function notionFetch(path, options) {
  const res = await fetch(`${NOTION_API_BASE}${path}`, { /* ... */ });
  return res.json();
}
Tips
ビルド時(SSG)のレート制限は必要だが、Edge Runtime のランタイムでは不要。環境によって戦略を変えること。

4. 施策③: OGPフェッチのタイムアウト短縮 — ✅ 微改善

リンクカード生成のためのOGPフェッチタイムアウトを3秒→1.5秒に短縮。

TypeScript
// Before
const response = await fetch(url, { signal: AbortSignal.timeout(3000) });

// After
const response = await fetch(url, { signal: AbortSignal.timeout(1500) });

遅いサイトのOGPを待つ時間を削減。効果は小さいが、最悪ケースの改善に寄与。

5. 施策④: Cache-Controlヘッダー — ❌ 効果なし

next.config.jsのheaders()でCDNキャッシュ用ヘッダーを設定。

JavaScript
async headers() {
  return [{
    source: '/blogs/:path*',
    headers: [
      { key: 'Cache-Control', value: 'public, s-maxage=3600, stale-while-revalidate=86400, max-age=0' },
    ],
  }];
}

結果: 効果なし。 Cloudflare PagesのWorker応答はCDNキャッシュレイヤーを通らないため、s-maxageを設定してもCDNにキャッシュされなかった。

重要
Cloudflare Pages の動的応答(Worker 経由)は、Cache-Control ヘッダーだけでは CDN キャッシュされません。これは重要な落とし穴です。ヘッダーは正しく返っているのに cf-cache-status: DYNAMIC のままだったら、まさにこの状態です。

6. 施策⑤: Cloudflare Cache Rule — ✅ 劇的効果

CloudflareダッシュボードからCache Ruleを設定。これが最も効果が大きかった

設定手順

  1. Cloudflare Dashboard → ゾーン選択 → CachingCache Rules
  2. Create rule
  3. 条件: URI Path starts with /blogs/ OR /categories/ OR /tags/ OR /archives/
  4. Edge TTL: Ignore cache-control header and use this TTL3600
  5. Deploy
注意
この条件だと TOP ページ(/)が入りません。上の計測で TOP の改善幅が小さいのはそのためです。TOP も対象にするなら、パスの前方一致ではなく「ホスト名が一致する」条件にして、除外したいもの(/api/ や RSC リクエスト)を AND で抜く形にします。
重要
RSC リクエストを除外すること。Next.js の App Router は、クライアント遷移時に同じ URL に ?_rsc=... を付けて別形式のデータを取りにいきます。これを HTML と同じキーでキャッシュすると、直リンクで開いた人に HTML ではなく RSC ペイロードが返る事故になります。

効果

ページ 施策①②後 Cache Rule後 改善率
記事詳細 2.20s 0.064s 34倍
記事一覧 1.21s 0.073s 17倍
カテゴリ 0.85s 0.067s 13倍

cf-cache-status: HITが返り、Notion APIを呼ばずにCDNから直接返却。

7. 最終結果まとめ

ページ 改善前 最終 改善率
記事詳細 4.23s 0.064s 66倍
記事一覧 2.23s 0.073s 30倍
カテゴリ 1.50s 0.067s 22倍

Tips

Tips
Cache Rule は無料プランでも使える。API 権限がなくてもダッシュボードから設定可能
Tips
初回アクセス(MISS)だけは 1〜2 秒かかる。許容できない場合は cron 等でキャッシュを温める仕組みが必要
Tips
CDN-Cache-Control ヘッダーも Pages Worker 応答では効かない。Cache Rule を使うこと
Tips
Edge Runtime のグローバル変数はリクエスト間で保持されない前提で設計する。レート制限の sleep はランタイムでは意味がない。逆に、キャッシュ目的でグローバル変数に貯めるのも危険(isolate が使い回されて古いデータが居座る)
注意
Cache Rule は一度入れたら終わりではありません。ホスティング先を乗り換えたり、プロジェクトを作り直したりすると、条件が合わなくなって黙って効かなくなります。遅いと感じたらまず curl -Icf-cache-status を見るのが早いです。

参考リンク

まとめ

  • 最も効果が大きいのはCloudflare Cache Rule(CDNキャッシュ強制)。これだけで20-60倍以上高速化
  • API並列化は堅実に40-50%改善。コード変更だけで済む
  • Cache-ControlヘッダーだけではPages Worker応答はキャッシュされない(重要な落とし穴)
  • Edge Runtimeのグローバル変数はリクエスト間で保持されないことを理解する
  • 初回アクセスの遅さが許容できるなら、Cache Ruleだけで十分な速度が得られる

更新履歴

  1. Cache Rule の条件例だと TOP ページが対象外になることと、RSC リクエストを除外しないと直リンクで RSC ペイロードが返る事故になることを警告として追加。Cache Rule がホスティング変更で黙って効かなくなる件を Tips に追加。引用形式の注意書き・Tips を callout に統一。グローバル変数の項に、キャッシュ目的での使用も危険であることを追記。自動目次を追加。参考リンクに関連記事を追加。「この記事でわかること」「対象読者」「前提条件」を概要プロパティへ移動。