📌📌 概要: 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環境で、どうやって高速化するか試行錯誤した記録をまとめる。
この記事でわかること
対象読者
前提条件
-
Next.js 15.1.0 + @cloudflare/next-on-pages
-
Cloudflare Pages(Edge Runtime必須)
-
Notion APIをCMSとして使用
-
export const runtime = 'edge' が全動的ルートに必要
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インスタンスが起動する。つまりグローバル変数でのレート制限は意味がない。
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();
}
async function notionFetch(path, options) {
const res = await fetch(`${NOTION_API_BASE}${path}`, { });
return res.json();
}
📌📌 ポイント: ビルド時(SSG)のレート制限は必要だが、Edge Runtimeのランタイムでは不要。環境によって戦略を変えること。
4. 施策③: OGPフェッチのタイムアウト短縮 — ✅ 微改善
リンクカード生成のためのOGPフェッチタイムアウトを3秒→1.5秒に短縮。
const response = await fetch(url, { signal: AbortSignal.timeout(3000) });
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にキャッシュされなかった。
📌⚠️ 注意: Cloudflare Pagesの動的応答(Worker経由)は、Cache-ControlヘッダーだけではCDNキャッシュされない。これは重要な落とし穴。
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
効果
| ページ |
施策①②後 |
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
📌📌 Tip 1: Cache Ruleは無料プランでも使える。API権限がなくてもダッシュボードから設定可能
📌📌 Tip 2: 初回アクセス(MISS)だけは1-2秒かかる。これが許容できない場合はcron等でキャッシュを温める仕組みが必要
📌📌 Tip 3: CDN-Cache-ControlヘッダーはCloudflare Pages Worker応答では効かない。Cache Ruleを使うこと
📌📌 Tip 4: Edge Runtimeのグローバル変数はリクエスト間で共有されない。レート制限等はランタイムでは不要
参考リンク
まとめ
-
最も効果が大きいのはCloudflare Cache Rule(CDNキャッシュ強制)。これだけで20-60倍以上高速化
-
API並列化は堅実に40-50%改善。コード変更だけで済む
-
Cache-ControlヘッダーだけではPages Worker応答はキャッシュされない(重要な落とし穴)
-
Edge Runtimeのグローバル変数はリクエスト間で保持されないことを理解する
-
初回アクセスの遅さが許容できるなら、Cache Ruleだけで十分な速度が得られる