はじめに
Next.jsのデプロイ先としてVercelを使っていたが、Cloudflare PagesがNext.jsに対応していると知って試してみた。
コスト・機能・制約の違いを実際に使って比べた。「Vercelで十分じゃないの?」という状態から始まって、どんな場面でCloudflare Pagesを選ぶかの判断基準が固まってきた。
Cloudflare PagesとVercelの概要
Vercel:
Next.jsを作ったVercel社のホスティング
Next.jsの全機能をサポート
自動プレビューデプロイ
Edge Functionsも対応
Cloudflare Pages:
Cloudflareのフロントエンドホスティング
Workers統合でエッジ処理が得意
無料枠が充実
Next.jsはサードパーティ対応(@cloudflare/next-on-pages)
料金比較
Vercel
Hobby(無料):
帯域幅: 100GB/月
関数の実行: 100GB-hours/月
ビルド時間: 6,000分/月
商用利用: 禁止
Pro($20/月):
帯域幅: 1TB/月
関数の実行: 1,000GB-hours/月
チームメンバー: 無制限
商用利用: 可能
Cloudflare Pages
Free(無料):
帯域幅: 無制限
リクエスト: 無制限
ビルド: 500回/月
商用利用: 可能
Pro($20/月):
ビルド: 5,000回/月
早いビルド
追加機能
無料枠の帯域幅が無制限というのがCloudflare Pagesの最大の強み。Vercelの無料枠は商用利用禁止なので、個人の商用プロジェクトはCloudflare Pagesのほうが使いやすい。
Cloudflare PagesへのNext.jsデプロイ
セットアップ
# @cloudflare/next-on-pagesをインストール
npm install -D @cloudflare/next-on-pages
// next.config.mjs
import { setupDevPlatform } from "@cloudflare/next-on-pages/next-dev";
/** @type {import('next').NextConfig} */
const nextConfig = {
// Cloudflare Pages用の設定
};
// 開発環境でWorkers bindingsを使う
if (process.env.NODE_ENV === "development") {
await setupDevPlatform();
}
export default nextConfig;
# wrangler.toml
name = "my-next-app"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = ".vercel/output/static"
[[kv_namespaces]]
binding = "MY_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
[[d1_databases]]
binding = "MY_DB"
database_name = "my-database"
database_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
// package.json
{
"scripts": {
"build": "next build",
"pages:build": "npx @cloudflare/next-on-pages",
"pages:deploy": "wrangler pages deploy",
"pages:preview": "wrangler pages dev",
"preview": "npm run pages:build && npm run pages:preview"
}
}
Edge Runtimeへの対応
Cloudflare PagesではすべてのサーバーサイドコードがEdge Runtimeで動く。これが一番大きな制約。
// ✓ Edge Runtimeで動くRoute Handler
// app/api/users/route.ts
export const runtime = "edge";
export async function GET(request: Request) {
// EdgeランタイムではNode.js APIが使えない
// fetch, URL, Request, Response, Headers等のWeb APIは使える
const res = await fetch("https://api.example.com/users");
const data = await res.json();
return Response.json(data);
}
// ✗ Edgeランタイムでは使えないもの
// import fs from "fs" → ✗ ファイルシステム不可
// import crypto from "crypto" → △ Web Crypto APIは使える
// import { Pool } from "pg" → ✗ Node.jsネイティブモジュール不可
// prisma / drizzle → △ Edgeサポートがあるものは動く
Cloudflare PagesでDBアクセスする場合
// app/api/users/route.ts
// D1(SQLite)はEdgeで使える
import { getRequestContext } from "@cloudflare/next-on-pages";
export const runtime = "edge";
export async function GET() {
const { env } = getRequestContext();
const db = env.MY_DB;
const result = await db.prepare(
"SELECT * FROM users ORDER BY created_at DESC LIMIT 20"
).all();
return Response.json(result.results);
}
export async function POST(request: Request) {
const { env } = getRequestContext();
const { name, email } = await request.json() as { name: string; email: string };
const db = env.MY_DB;
const user = await db.prepare(
"INSERT INTO users (name, email) VALUES (?, ?) RETURNING *"
).bind(name, email).first();
return Response.json(user, { status: 201 });
}
KVとD1をNext.jsから使う
// lib/cloudflare.ts
// Cloudflare bindingsへのアクセスをラップするユーティリティ
import { getRequestContext } from "@cloudflare/next-on-pages";
export function getCloudflareEnv() {
return getRequestContext().env;
}
// KVのラッパー
export async function kvGet(key: string): Promise<string | null> {
const { MY_KV } = getCloudflareEnv();
return MY_KV.get(key);
}
export async function kvSet(
key: string,
value: string,
ttl?: number,
): Promise<void> {
const { MY_KV } = getCloudflareEnv();
await MY_KV.put(key, value, ttl ? { expirationTtl: ttl } : undefined);
}
// app/api/session/route.ts
import { getCloudflareEnv, kvGet, kvSet } from "@/lib/cloudflare";
export const runtime = "edge";
export async function GET(request: Request) {
const sessionId = new URL(request.url).searchParams.get("id");
if (!sessionId) return Response.json({ error: "Session ID required" }, { status: 400 });
const session = await kvGet(`session:${sessionId}`);
if (!session) return Response.json({ error: "Session not found" }, { status: 404 });
return Response.json(JSON.parse(session));
}
VercelとCloudflare Pagesの機能比較
| 機能 | Vercel | Cloudflare Pages |
|---|---|---|
| Next.jsサポート | ◎ 公式 | △ サードパーティ |
| Node.js API | ◎ フル対応 | ✗ Edgeのみ |
| プレビューデプロイ | ◎ 自動 | ◎ 自動 |
| 帯域幅 | 100GB/月(無料) | 無制限(無料) |
| 商用利用(無料) | ✗ 禁止 | ✓ 可能 |
| Workers連携 | △ | ◎ シームレス |
| KV/D1/R2 | ✗ | ◎ |
| ビルド時間 | 速い | やや遅い |
| カスタムドメイン | 無料 | 無料 |
| ログ・分析 | ◎ | ○ |
どちらを選ぶか
Vercelを選ぶとき:
✓ Next.jsの全機能を使いたい(特にServer Actionsの高度な機能)
✓ Node.js固有のライブラリを使う(prisma, sharp等)
✓ チーム開発でプレビューの使い勝手を重視
✓ 個人ホビープロジェクト(商用でない)
Cloudflare Pagesを選ぶとき:
✓ 無料で商用利用したい
✓ Workers/KV/D1/R2との組み合わせで使う
✓ 帯域幅が多くなりそう
✓ Cloudflareで既にDNSを管理している
✓ グローバルに低レイテンシが必要
✓ Next.jsをEdge Runtimeのみで完結させられる
移行時の注意点
VercelからCloudflare Pagesに移行するときに詰まったポイント。
① Node.jsモジュールが使えない
// Vercelでは動くがCloudflare Pagesでは動かない例
// ✗ fsモジュール
import fs from "fs";
const file = fs.readFileSync("./data.json");
// ✓ 代替: publicフォルダに置いてfetchで読む
const res = await fetch("/data.json");
const data = await res.json();
② Prismaの対応
// Prismaはデフォルトではエッジ非対応
// @prisma/adapter-d1 を使うとD1で動く
import { PrismaClient } from "@prisma/client";
import { PrismaD1 } from "@prisma/adapter-d1";
export const runtime = "edge";
export async function GET() {
const { env } = getRequestContext();
const adapter = new PrismaD1(env.MY_DB);
const prisma = new PrismaClient({ adapter });
const users = await prisma.user.findMany();
return Response.json(users);
}
③ ImageResponseが動かない場合
// OG画像生成(@vercel/og)は一部制約がある
// Cloudflare Pagesでは動作確認が必要
export const runtime = "edge";
import { ImageResponse } from "next/og";
export async function GET() {
return new ImageResponse(
<div style={{ display: "flex" }}>Hello OG</div>,
{ width: 1200, height: 630 }
);
}
GitHub ActionsでCloudflare Pagesにデプロイ
# .github/workflows/pages-deploy.yml
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build Next.js
run: npm run build
env:
NODE_ENV: production
- name: Build for Cloudflare Pages
run: npx @cloudflare/next-on-pages
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy .vercel/output/static --project-name=my-next-app
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
Cloudflare PagesとWorkersを組み合わせる
典型的な構成:
[ブラウザ]
↓
[Cloudflare Pages(Next.js)] ← フロントエンド
↓ API呼び出し
[Cloudflare Workers(Hono)] ← APIレイヤー
↓
[D1 / R2 / KV / 外部API]
// Next.jsからWorkersを呼ぶ
// app/api/data/route.ts
export const runtime = "edge";
export async function GET(request: Request) {
// 同一Cloudflareアカウントの別WorkerをService Bindingで呼べる
const { env } = getRequestContext();
// Service Bindingがあれば内部ネットワークで呼べる(外部への通信なし)
// const result = await env.MY_WORKER.fetch(request);
// 通常のfetchでも可
const res = await fetch("https://my-worker.my-subdomain.workers.dev/data");
const data = await res.json();
return Response.json(data);
}
まとめ
- Cloudflare Pagesは無料で商用利用可能かつ帯域幅無制限が最大の強み
- Node.js APIが使えないEdgeの制約がVercelとの最大の違い
- Workers/KV/D1/R2との統合はCloudflare Pagesが自然
- Next.jsの全機能(特にServer Components + Node.js API)が必要ならVercel
- Cloudflareで既にDNS管理しているなら設定がシンプル
個人的な使い分けとして「Node.jsライブラリが必要または複雑なServer Actions → Vercel」「シンプルなNext.js + Cloudflareのエコシステムを使いたい → Cloudflare Pages」という基準に落ち着いた。