WithCodeMedia-1-pc
previous arrowprevious arrow
next arrownext arrow

WithCodeMedia-1-sp
previous arrowprevious arrow
next arrownext arrow

Edge Runtime(Cloudflare Workers)でパーソナライズ・エッジレンダリングを実装する方法【完全ガイド】|Next.js Middleware・AB テスト実例付き

目次

この記事でわかること

  • Edge Runtimeとは何か・通常のサーバーレスとのレイテンシ・コスト比較
  • Cloudflare Workers・Vercel Edge Functions・Netlify Edge Functionsの選び方
  • Next.js Middlewareによる認証制御・ABテスト・ジオロケーション分岐の実装
  • Cloudflare Workers + Honoフレームワーク・KV・D1連携の実装手順
  • CPU時間制限対応・waitUntil活用・wranglerデプロイ・モニタリング方法
生徒

ログイン状態によるページ分岐をサーバーで処理しているんですが、レスポンスが遅くて……もっと速くできませんか?

ペン博士

Edge Runtimeを使えばユーザーに最も近いエッジサーバーで処理できるんじゃ!認証チェック・リダイレクト・ABテストのような軽量処理はエッジで即座に判断することで、数msレベルのレスポンスが実現できるぞ。Cloudflare WorkersやNext.js Middlewareで簡単に始められるんじゃ!

結論:Edge Runtimeはユーザーに物理的に近いCDNノードでコードを実行するため、コールドスタートほぼゼロ・数msレベルのレスポンスを実現します。認証チェック・リダイレクト・ABテスト・ジオロケーション分岐などの軽量処理をエッジに移すことで、ユーザー体験を大幅に改善できます。

本記事では、Edge Runtimeの仕組みと主要プラットフォーム(Cloudflare Workers・Vercel Edge Functions・Netlify Edge Functions)の比較・Next.js Middlewareの実装・認証制御・ABテスト・ジオロケーション分岐・Cloudflare KV/D1との連携・Honoフレームワーク・デプロイとモニタリングを完全解説します。


Edge Runtimeとは|通常のサーバーレスとの違い

Edge Runtimeとは、CDN(コンテンツデリバリーネットワーク)のエッジノード上でコードを実行できる仕組みです。Cloudflareであれば世界310箇所以上のエッジロケーションにデプロイでき、どのユーザーにも物理的に近いサーバーでリクエストを処理できます。

通常のAWS Lambda(東京リージョン)を使った場合、米国ユーザーからのアクセスには150〜200msのレイテンシが発生します。Edge Runtimeを使えばこのレイテンシを10〜30msに削減できます。コールドスタートも数ms以下(Lambdaは数百ms〜1秒)なので、パフォーマンス面で大きなアドバンテージがあります。

Edge RuntimeとリージョンサーバーレスのCDNノード配置の比較図
項目通常のサーバーレス(AWS Lambda等)Edge Runtime(Cloudflare Workers等)
実行場所特定リージョンのデータセンター世界200+箇所のエッジノード
コールドスタート数百ms〜1秒数ms(ほぼゼロ)
メモリ上限128MB〜3GB128MB(軽量処理向け)
ランタイムNode.js / Python等フル環境V8(Node.js APIの一部は使えない)
向いている処理DB接続・ファイル操作・重い処理認証・リダイレクト・ABテスト・軽量API
課金モデル実行時間(ms)×メモリリクエスト数ベース(Cloudflare Workers無料枠10万req/日)

主要プラットフォームの比較|どれを選ぶべきか

>【主要 Edge Runtime プラットフォーム比較】

Cloudflare Workers:
→ WASMベース。世界310+エッジロケーション
→ CPU制限:10ms(バースト50ms)
→ KV・D1(SQLite)・R2(オブジェクトストレージ)・Queues・Durable Objectsとの連携が容易
→ Hono フレームワークとの相性が最良
→ 無料枠:10万リクエスト/日

Vercel Edge Functions:
→ Next.js Middlewareとして組み込み済み
→ TypeScript ネイティブ対応
→ Next.js を使っているなら追加設定不要
→ 無料枠:Edge Middlewareは100,000実行/月

Netlify Edge Functions:
→ Denoベース(Node.js互換性あり)
→ TypeScriptをそのまま書ける
→ Netlify を使っているプロジェクトに最適
→ 無料枠:125,000回/月

選択指針:
→ Next.js 使用中 → Vercel Edge Functions (Next.js Middleware)
→ フレームワーク非依存・グローバル展開 → Cloudflare Workers
→ Netlify デプロイ中 → Netlify Edge Functions

Next.js Middlewareで実装する3つの実例

Next.js Middlewareによるエッジ処理フローの図

実例1:認証チェックとリダイレクト制御

>// middleware.ts(プロジェクトルートに配置)

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'  // Edge対応のJWTライブラリ

const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!)

export async function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  // 管理画面への認証チェック
  if (pathname.startsWith('/admin') || pathname.startsWith('/dashboard')) {
    const token = request.cookies.get('auth_token')?.value

    if (!token) {
      // 未認証ならログインページへリダイレクト
      const loginUrl = new URL('/login', request.url)
      loginUrl.searchParams.set('redirect', pathname)
      return NextResponse.redirect(loginUrl)
    }

    // JWTの検証(jose はEdge対応)
    // ⚠️ Node.jsの crypto モジュールはEdgeでは使えないため jose を使う
    try {
      const { payload } = await jwtVerify(token, JWT_SECRET)

      // ロールベースのアクセス制御
      if (pathname.startsWith('/admin') && payload.role !== 'admin') {
        return NextResponse.redirect(new URL('/unauthorized', request.url))
      }

      // 認証情報をヘッダーに付与してバックエンドに渡す
      const requestHeaders = new Headers(request.headers)
      requestHeaders.set('x-user-id', String(payload.sub))
      requestHeaders.set('x-user-role', String(payload.role))

      return NextResponse.next({ request: { headers: requestHeaders } })
    } catch {
      // JWTが無効(期限切れ・改ざん)ならログインページへ
      const response = NextResponse.redirect(new URL('/login', request.url))
      response.cookies.delete('auth_token')
      return response
    }
  }

  return NextResponse.next()
}

// Middlewareを適用するパスを指定
export const config = {
  matcher: [
    '/admin/:path*',
    '/dashboard/:path*',
    // 静的ファイルとAPIルートを除外
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

実例2:ABテストのルーティング

>// middleware.ts - A/Bテストの実装

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// ABテストの設定
const AB_TESTS = {
  homepage: {
    variants: ['control', 'variant-b', 'variant-c'],
    weights: [50, 30, 20],  // 配分(合計100)
  }
}

function selectVariant(weights: number[]): number {
  const total = weights.reduce((a, b) => a + b, 0)
  let random = Math.random() * total
  for (let i = 0; i < weights.length; i++) {
    random -= weights[i]
    if (random <= 0) return i
  }
  return 0
}

export function middleware(request: NextRequest) {
  // トップページへのリクエストのみ対象
  if (request.nextUrl.pathname !== '/') {
    return NextResponse.next()
  }

  // 既存のバリアントCookieを確認
  const existingVariant = request.cookies.get('ab_homepage')?.value
  const config = AB_TESTS.homepage

  const variantName = existingVariant && config.variants.includes(existingVariant)
    ? existingVariant
    : config.variants[selectVariant(config.weights)]

  // バリアントに対応するURLにリライト
  const url = request.nextUrl.clone()
  if (variantName !== 'control') {
    url.pathname = `/${variantName}`
  }

  const response = NextResponse.rewrite(url)

  // バリアントをCookieに保存(30日間)
  if (!existingVariant) {
    response.cookies.set('ab_homepage', variantName, {
      maxAge: 60 * 60 * 24 * 30,
      httpOnly: true,
      sameSite: 'lax',
    })
  }

  // レスポンスヘッダーでバリアント情報を付与(アナリティクス用)
  response.headers.set('x-ab-variant', variantName)

  return response
}

実例3:ジオロケーション分岐とパーソナライズ

>// middleware.ts - ジオロケーション分岐(Vercel / Cloudflare で利用可能)

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// 対応言語とリージョンのマッピング
const LOCALE_MAP: Record = {
  JP: 'ja',
  US: 'en',
  GB: 'en',
  DE: 'de',
  FR: 'fr',
  CN: 'zh',
  KR: 'ko',
}

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  // 既に言語プレフィックスがあればスキップ
  const hasLocalePrefix = /^\/(ja|en|de|fr|zh|ko)/.test(pathname)
  if (hasLocalePrefix) return NextResponse.next()

  // Vercel が自動付与するジオロケーション情報
  const country = request.geo?.country || 'US'
  const city = request.geo?.city || ''

  // Accept-Language ヘッダーからも判定
  const acceptLanguage = request.headers.get('accept-language') || ''
  const preferredLang = acceptLanguage.split(',')[0].split('-')[0].toLowerCase()

  // ロケールを決定(country優先、次にaccept-language)
  const locale = LOCALE_MAP[country] ?? preferredLang ?? 'en'

  // ロケールプレフィックス付きURLにリダイレクト
  if (locale !== 'en') {
    const url = request.nextUrl.clone()
    url.pathname = `/${locale}${pathname}`
    const response = NextResponse.redirect(url)
    response.headers.set('x-user-country', country)
    response.headers.set('x-user-city', city)
    return response
  }

  // レスポンスヘッダーに地域情報を追加(アナリティクス用)
  const response = NextResponse.next()
  response.headers.set('x-user-country', country)
  response.headers.set('x-user-city', city)

  return response
}

Cloudflare Workers + Hono フレームワーク

HonoはCloudflare Workersに最適化された超軽量Webフレームワークです。Expressライクなシンプルなルーティング・ミドルウェア・TypeScript対応を提供しながら、バンドルサイズはわずか数KBです。

># Cloudflare Workers + Hono プロジェクトの作成
npm create cloudflare@latest my-hono-worker -- --framework=hono
cd my-hono-worker
>// src/index.ts - Hono を使った Cloudflare Workers

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { jwt } from 'hono/jwt'

type Bindings = {
  KV: KVNamespace   // Cloudflare KV
  DB: D1Database    // Cloudflare D1 (SQLite)
}

const app = new Hono<{ Bindings: Bindings }>()

// ミドルウェアの設定
app.use('*', logger())
app.use('/api/*', cors({
  origin: ['https://myapp.com', 'https://staging.myapp.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
}))

// JWTミドルウェア(認証が必要なルート)
app.use('/api/protected/*', jwt({ secret: c => c.env.JWT_SECRET }))

// ルーティング
app.get('/', (c) => c.text('Hello Edge!'))

// パーソナライズAPI
app.get('/api/personalize', async (c) => {
  const country = c.req.header('cf-ipcountry') || 'US'
  const ua = c.req.header('user-agent') || ''
  const isMobile = /iPhone|Android|iPad/.test(ua)

  // Cloudflare KV からユーザー設定を取得(超高速)
  const userId = c.get('jwtPayload')?.sub
  const userConfig = userId
    ? await c.env.KV.get(`user:${userId}:config`, 'json')
    : null

  return c.json({
    country,
    device: isMobile ? 'mobile' : 'desktop',
    userConfig,
    timestamp: new Date().toISOString(),
  })
})

// Cloudflare D1 (SQLite) を使ったAPI
app.get('/api/articles', async (c) => {
  const country = c.req.query('country') || 'JP'

  // D1にSQLクエリ(エッジで直接DBアクセス)
  const { results } = await c.env.DB
    .prepare('SELECT * FROM articles WHERE country = ? LIMIT 10')
    .bind(country)
    .all()

  return c.json({ articles: results })
})

export default app

Cloudflare KVの活用

Cloudflare KV(Key-Value Store)はエッジで使える分散型のKVストレージです。読み取りはエッジノードでキャッシュされるため、グローバルに高速なデータ読み取りが可能です。設定値・フィーチャーフラグ・ユーザーセッションの保存に適しています。

>// KV の基本操作

// wrangler.toml でKVをバインド
// [[kv_namespaces]]
// binding = "KV"
// id = "xxxxxxxxxxxxx"

// 書き込み(TTL付き)
await env.KV.put('feature:new-ui', 'true', { expirationTtl: 3600 })

// 読み取り
const isNewUi = await env.KV.get('feature:new-ui')  // → 'true' or null

// JSON の読み書き
await env.KV.put('config:ab-test', JSON.stringify({ variant: 'b', ratio: 0.5 }))
const config = await env.KV.get('config:ab-test', 'json')

// キーの一覧
const keys = await env.KV.list({ prefix: 'user:' })

// 削除
await env.KV.delete('feature:new-ui')

Edge Runtimeのパフォーマンス最適化

CPU時間制限への対応

Cloudflare WorkersのCPU時間制限は、バースト時でも50msです。これを超えると処理が強制終了されます。Edge Runtimeでは必ず「軽量な処理のみをエッジで行い、重い処理はオリジンサーバーに委譲する」設計が重要です。

>【Edge で行うべき処理 vs オリジンに委譲すべき処理】

✅ Edgeで行うべき処理(10ms以内で完了するもの):
→ JWT/Cookie の検証(josや軽量ライブラリ使用)
→ リダイレクト・リライト
→ レスポンスヘッダーの付与
→ ABテストのバリアント割り当て
→ ジオロケーション分岐
→ KV/D1 からの設定読み込み(単純なクエリ)

❌ オリジンに委譲すべき処理:
→ 重いDBクエリ(結合・集計など)
→ 外部APIへの大量リクエスト
→ 画像・動画の処理
→ 重い計算処理(機械学習推論など)
→ ファイルシステムへのアクセス

waitUntil()でレスポンス後に処理を継続する

>// waitUntil を使うとレスポンスを返した後に処理を継続できる

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
    // まず素早くレスポンスを返す
    const response = new Response('OK', { status: 200 })

    // レスポンス後にアナリティクスデータを非同期送信
    // → ユーザーを待たせずにバックグラウンドで処理
    ctx.waitUntil(
      sendAnalytics(request, env)  // 非同期処理
    )

    return response
  }
}

async function sendAnalytics(request: Request, env: Env) {
  const data = {
    url: request.url,
    country: request.headers.get('cf-ipcountry'),
    timestamp: Date.now(),
  }
  await env.KV.put(`analytics:${Date.now()}`, JSON.stringify(data), {
    expirationTtl: 86400  // 24時間後に自動削除
  })
}

デプロイとモニタリング

wranglerコマンドでのデプロイ

># wrangler(Cloudflare公式CLIツール)のインストール
npm install -g wrangler

# ログイン
wrangler login

# ローカル開発サーバーの起動(本番環境と同じ挙動)
wrangler dev

# ステージング環境へのデプロイ
wrangler deploy --env staging

# 本番環境へのデプロイ
wrangler deploy --env production

# ログのリアルタイム確認(デプロイ後のデバッグ)
wrangler tail

wrangler.tomlの設定例

># wrangler.toml - Cloudflare Workers の設定ファイル

name = "my-hono-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]  # Node.js互換モード

# KV ストレージのバインド
[[kv_namespaces]]
binding = "KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# D1 データベースのバインド
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# 環境変数(シークレットは wrangler secret で設定)
[vars]
ENVIRONMENT = "production"

# ステージング環境の設定
[env.staging]
name = "my-hono-worker-staging"
[env.staging.vars]
ENVIRONMENT = "staging"

よくある質問(FAQ)

Q1. Edge RuntimeでNode.js APIは全部使えますか?

A. 多くのNode.js固有のAPIは使えません。主に使えないのは fs(ファイルシステム)・child_processnetcrypto(一部)などです。使えるのはWebブラウザの標準API(fetchcrypto.subtleTextEncoder等)と一部のNode互換APIのみです。Cloudflare Workersはcompatibility_flags = ["nodejs_compat"]を設定することで多くのNode.jsモジュールを利用できます。

Q2. エッジでデータベースにアクセスできますか?

A. 従来のTCP接続を使うDBへの直接アクセスはできません。ただし Cloudflare D1(SQLite互換)・PlanetScaleNeon(HTTP接続対応のPostgreSQL)・Turso(エッジ対応SQLite)など、エッジ対応DBを使うことでデータアクセスが可能です。単純なKVアクセスならCloudflare KVやDurable Objectsが最も高速です。

Q3. Cloudflare Workers の無料枠はどのくらいですか?

A. 無料プランでは1日10万リクエストまで無料(Workers Free)。有料プランでは月$5(Workers Paid)で100万リクエストが含まれ、超過分は100万リクエストあたり$0.30です。KVは無料枠で100,000読み取り/日・1,000書き込み/日が含まれます。個人サイトや中規模サービスなら無料枠で十分カバーできます。

Q4. Edge Runtimeは日本のユーザーに対して有効ですか?

A. 非常に有効です。Cloudflareは東京・大阪を含む複数の日本のエッジノードを持っています。東京のユーザーが東京のエッジノードでリクエストを処理される場合、レイテンシは数ms以下です。一方、オリジンサーバーが米国の場合は往復100ms以上かかります。国内ユーザーへの高速化にも大きな効果があります。

Q5. Next.js MiddlewareとAPI Routesはどう使い分けますか?

A. MiddlewareはリクエストがページやAPIに届く前に実行される軽量処理(認証確認・リダイレクト・ヘッダー付与など)に使います。API RoutesはDB接続・外部API呼び出し・複雑なビジネスロジックなど、より重い処理に使います。原則として「ルーティング判断・認証確認・ヘッダー操作はMiddleware、実際のデータ処理はAPI Route」と覚えると整理しやすいです。


まとめ

  • Edge Runtime:CDNノードでコードを実行する仕組み。コールドスタートほぼゼロ・数msレベルのレスポンスが可能。無料枠が充実していてコスト効率が高い。
  • 向いている処理:認証チェック・リダイレクト・ABテスト・ジオロケーション分岐・レスポンスヘッダー付与など軽量な処理。
  • Next.js Middlewaremiddleware.tsを配置するだけでEdge処理を組み込める。matcherでパスを絞る。
  • Cloudflare Workers + Hono:超軽量フレームワークでExpressライクなルーティングを実装できる。KV・D1・R2との連携が容易。
  • waitUntil:レスポンス後にバックグラウンドで処理を継続できる。アナリティクス送信などに活用。
  • 注意点:Node.js固有のAPIは使えない。重い処理・DB接続はリージョンサーバーに委譲する設計が重要。CPU制限(10ms/50ms)を意識する。

Edge Runtimeは「全部エッジで処理しよう」ではなく、「軽量なリクエスト処理はエッジ・重い処理はオリジン」という使い分けが成功の鍵です。まずNext.js Middlewareでの認証制御やABテストから試してみましょう。Cloudflare WorkersとHonoの組み合わせは超高速APIの構築に最適です。


WithCodeを体験できる初級コース公開中!

この記事を書いた人

WithCode(ウィズコード)は「目指すなら稼げる人材」をビジョンに、累計400名以上のフリーランスを輩出してきた超実践型プログラミングスクールです。150社以上の実案件支援を特徴にWeb制作・Webデザインなどの役立つ情報を現場のノウハウに基づいて発信していきます。

– service –WithGroupの運営サービス

  • WithCode
    - ウィズコード -

    スクール

    「未経験」から
    現場で通用する
    スキルを身に付けよう!

    詳細はこちら
  • WithFree
    - ウィズフリ -

    実案件サポート

    制作会社のサポート下で
    実務経験を積んでいこう!

    詳細はこちら
  • WithCareer
    - ウィズキャリ -

    就転職サポート

    大手エージェントのサポート下で
    キャリアアップを目指そう!

    詳細はこちら

公式サイト より
今すぐ
無料カウンセリング
予約!

目次