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

この記事でわかること
- 記事ページの 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呼び出しが直列で走っていた:
- Notion API: 記事データ取得(Slug検索)
- Notion API: ブロック一覧取得(本文)
- Notion API: 記事一覧取得(サイドバー用)
- 外部サイト: 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
const blog = await getDetail(blogId);
const { contents } = await getList();After
const [blog, { contents }] = await Promise.all([
getDetail(blogId),
getList(),
]);同様にカテゴリ・タグページでも並列化:
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インスタンスが起動する。つまりグローバル変数でのレート制限は意味がない。
// 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();
}4. 施策③: OGPフェッチのタイムアウト短縮 — ✅ 微改善
リンクカード生成のためのOGPフェッチタイムアウトを3秒→1.5秒に短縮。
// 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キャッシュ用ヘッダーを設定。
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にキャッシュされなかった。
Cache-Control ヘッダーだけでは CDN キャッシュされません。これは重要な落とし穴です。ヘッダーは正しく返っているのに cf-cache-status: DYNAMIC のままだったら、まさにこの状態です。6. 施策⑤: Cloudflare Cache Rule — ✅ 劇的効果
CloudflareダッシュボードからCache Ruleを設定。これが最も効果が大きかった。
設定手順
- Cloudflare Dashboard → ゾーン選択 → Caching → Cache Rules
- Create rule
- 条件: URI Path starts with
/blogs/OR/categories/OR/tags/OR/archives/ - Edge TTL: Ignore cache-control header and use this TTL →
3600秒 - Deploy
/)が入りません。上の計測で TOP の改善幅が小さいのはそのためです。TOP も対象にするなら、パスの前方一致ではなく「ホスト名が一致する」条件にして、除外したいもの(/api/ や RSC リクエスト)を AND で抜く形にします。?_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
CDN-Cache-Control ヘッダーも Pages Worker 応答では効かない。Cache Rule を使うことcurl -I で cf-cache-status を見るのが早いです。参考リンク
- Cloudflare Cache Rules ドキュメント
- next-on-pages GitHub
- Cloudflare CDN-Cache-Control
- Cloudflare Workers→Pages出戻り記(別記事)
まとめ
- 最も効果が大きいのはCloudflare Cache Rule(CDNキャッシュ強制)。これだけで20-60倍以上高速化
- API並列化は堅実に40-50%改善。コード変更だけで済む
Cache-ControlヘッダーだけではPages Worker応答はキャッシュされない(重要な落とし穴)- Edge Runtimeのグローバル変数はリクエスト間で保持されないことを理解する
- 初回アクセスの遅さが許容できるなら、Cache Ruleだけで十分な速度が得られる
更新履歴
- Cache Rule の条件例だと TOP ページが対象外になることと、RSC リクエストを除外しないと直リンクで RSC ペイロードが返る事故になることを警告として追加。Cache Rule がホスティング変更で黙って効かなくなる件を Tips に追加。引用形式の注意書き・Tips を callout に統一。グローバル変数の項に、キャッシュ目的での使用も危険であることを追記。自動目次を追加。参考リンクに関連記事を追加。「この記事でわかること」「対象読者」「前提条件」を概要プロパティへ移動。


