ブログ記事

Edge Function完全マスターガイド2025 - Cloudflare Workers vs Vercel Edge Functions

Edge Functionの基礎から実践的な活用方法まで徹底解説。Cloudflare Workers、Vercel Edge Functionsの比較、認証・API Gateway・パーソナライゼーションの実装例、パフォーマンス最適化まで、エッジコンピューティングを活用した高速なWebアプリケーション構築に必要な全知識を網羅。

22分で読めます
R
Rina
Daily Hack 編集長
Web開発
Edge Functions Cloudflare Workers Vercel API Gateway エッジコンピューティング
Edge Function完全マスターガイド2025 - Cloudflare Workers vs Vercel Edge Functionsのヒーロー画像

エッジコンピューティングの進化により、Web アプリケーションのパフォーマンスとユーザー体験は新たな次元に到達しました。2025 年現在、Edge Function は単なるパフォーマンス最適化ツールを超えて、認証、パーソナライゼーション、リアルタイム処理など、多様なユースケースに対応する必須技術となっています。本記事では、主要プラットフォームの最新機能比較から実践的な実装パターンまで、包括的に解説します。

この記事で学べること

  • Edge Function の基本概念と 2025 年の最新動向
  • Cloudflare Workers と Vercel Edge Functions の詳細比較
  • 認証・API Gateway・パーソナライゼーションの実装
  • パフォーマンス最適化とコスト削減の戦略
  • 実践的なユースケースと実装パターン

目次

  1. Edge Function とは
  2. 主要プラットフォームの最新アップデート(2025 年版)
  3. プラットフォーム比較:Cloudflare Workers vs Vercel Edge Functions
  4. 環境構築とセットアップ
  5. 基本実装:Hello Edge から始める
  6. 認証システムの実装
  7. API Gateway としての活用
  8. パーソナライゼーションと A/B テスト
  9. パフォーマンス最適化戦略
  10. コスト管理とスケーリング
  11. 実践例:グローバル対応 Web アプリケーション
  12. まとめとベストプラクティス

Edge Functionとは

Edge Function は、CDN のエッジロケーション上で実行されるサーバーレス関数です。ユーザーに最も近い場所でコードを実行することで、レイテンシを大幅に削減し、グローバルスケールでの高速なレスポンスを実現します。

従来のサーバーレスとの違い

サーバーレスとEdge Functionの比較
特徴 従来のサーバーレス Edge Function
実行場所 特定リージョンのデータセンター グローバルCDNエッジ
レイテンシ 50-500ms 5-50ms
コールドスタート あり(100-1000ms) ほぼなし(5-50ms)
実行時間制限 最大15分 最大50ms-30秒
メモリ制限 最大10GB 128MB-256MB
ユースケース バッチ処理・重い計算 リアルタイム処理・軽量タスク

Edge Functionの動作原理

Edge Functionのリクエスト処理フロー

チャートを読み込み中...

主要プラットフォームの最新アップデート(2025年版)

Cloudflare Workers 2025

2025 年 6 月、Cloudflareは大きな転換点を迎えます。

Workers KV 3倍高速化

静的アセットの配信が大幅に高速化

Workers Logs Open Beta

ダッシュボード内での完全な検索・フィルタリング機能

コンテナサポート開始

Workersとコンテナのハイブリッドワークロードをサポート

V8 13.8アップデート

Float16Array対応など最新のJavaScript機能

重要なアップデート

Service Bindings と Tail Workers へのリクエストが無料化され、マイクロサービスアーキテクチャの構築コストが大幅に削減されました。

Vercel Edge Functions 2025

Vercelは「Fluid Compute」モデルの導入により、パフォーマンスを新たなレベルに引き上げました。

コールドスタート削減 40 %
コスト削減(対Serverless Functions) 15 %
グローバルカバレッジ 95 %

主要機能:

  • Fluid Compute: 複数の実行が単一インスタンスを共有
  • バイトコードキャッシング: Node.js 20+でコンパイル済みコードを保存
  • クロスリージョンフェイルオーバー: 自動的な可用性ゾーン間の切り替え
  • waitUntilサポート: バックグラウンドタスクの非同期実行

プラットフォーム比較:Cloudflare Workers vs Vercel Edge Functions

詳細比較表

Cloudflare WorkersとVercel Edge Functionsの詳細比較(2025年版)
項目 Cloudflare Workers Vercel Edge Functions
グローバル展開 200+ロケーション 20+リージョン
実行時間制限 30秒(有料)/10秒(無料) 25秒初期レスポンス後は無制限
メモリ制限 128MB 256MB(Netlify経由)
CPU時間制限 50ms(無料)/30秒(有料) なし(初期レスポンス内)
言語サポート JavaScript, TypeScript, Rust, C++ JavaScript, TypeScript
価格 $5/月〜 + 従量課金 $20/月〜 + 従量課金
開発体験 Wrangler CLI, Web IDE Vercel CLI, フレームワーク統合
特徴 コンテナサポート(2025年6月) Fluid Compute, Next.js統合

アーキテクチャの違い

興味深いことに、Vercel Edge Functions は Cloudflare Workers の上に構築されています。しかし、それぞれ独自の最適化と機能を提供しています。

// Cloudflare Workersの基本構造 export default { async fetch(request, env, ctx) { // 直接的なfetchハンドラー // envで環境変数とバインディング // ctxでwaitUntilなど return new Response("Hello from Workers!"); } }
// Vercel Edge Functionsの基本構造 import { NextRequest } from 'next/server'; export const config = { runtime: 'edge', }; export default function handler(req: NextRequest) { // Next.js統合された型定義 // 自動的なデプロイとルーティング return new Response('Hello from Edge!'); }
Cloudflare Workers
// Cloudflare Workersの基本構造 export default { async fetch(request, env, ctx) { // 直接的なfetchハンドラー // envで環境変数とバインディング // ctxでwaitUntilなど return new Response("Hello from Workers!"); } }
Vercel Edge Functions
// Vercel Edge Functionsの基本構造 import { NextRequest } from 'next/server'; export const config = { runtime: 'edge', }; export default function handler(req: NextRequest) { // Next.js統合された型定義 // 自動的なデプロイとルーティング return new Response('Hello from Edge!'); }

環境構築とセットアップ

Cloudflare Workersのセットアップ

# Wrangler CLIのインストール
npm install -g wrangler

# プロジェクトの初期化
wrangler init my-worker

# ローカル開発
wrangler dev

# デプロイ
wrangler deploy

Vercel Edge Functionsのセットアップ

# Vercel CLIのインストール
npm install -g vercel

# Next.jsプロジェクトの作成
npx create-next-app@latest my-edge-app

# Edge Functionの作成
# app/api/edge/route.ts または pages/api/edge.ts

# デプロイ
vercel

基本実装:Hello Edgeから始める

// src/index.ts export interface Env { MY_KV: KVNamespace; MY_SECRET: string; } export default { async fetch( request: Request, env: Env, ctx: ExecutionContext ): Promise<Response> { const url = new URL(request.url); // パスベースのルーティング switch (url.pathname) { case '/api/hello': return new Response(JSON.stringify({ message: 'Hello from Cloudflare Workers!', location: request.cf?.city || 'Unknown', timestamp: new Date().toISOString() }), { headers: { 'Content-Type': 'application/json' } }); case '/api/cached': // KVストレージの使用 const cached = await env.MY_KV.get('data'); if (cached) { return new Response(cached); } const data = JSON.stringify({ generated: Date.now() }); // 1時間キャッシュ ctx.waitUntil( env.MY_KV.put('data', data, { expirationTtl: 3600 }) ); return new Response(data); default: return new Response('Not Found', { status: 404 }); } } };
// app/api/edge/route.ts import { NextRequest } from 'next/server'; export const runtime = 'edge'; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const name = searchParams.get('name') || 'World'; // 地理情報の取得 const country = request.geo?.country || 'Unknown'; const city = request.geo?.city || 'Unknown'; // レスポンスヘッダーの設定 const response = new Response( JSON.stringify({ message: `Hello ${name} from Vercel Edge!`, location: { country, city }, timestamp: new Date().toISOString(), // Fluid Computeの恩恵を受ける instanceId: crypto.randomUUID() }), { status: 200, headers: { 'Content-Type': 'application/json', 'Cache-Control': 's-maxage=10, stale-while-revalidate', }, } ); return response; } export async function POST(request: NextRequest) { const body = await request.json(); // バックグラウンドタスクの実行 const response = new Response( JSON.stringify({ received: body, processed: true }) ); // waitUntilを使用したログ送信 // (Vercelでは自動的に処理される) return response; }
// netlify/edge-functions/hello.ts import type { Context } from "https://edge.netlify.com"; export default async (request: Request, context: Context) => { const { city, country } = context.geo; // Denoランタイムを活用 const url = new URL(request.url); const name = url.searchParams.get("name") || "World"; // Netlify Blobsを使用したストレージ const blob = context.env.get("NETLIFY_BLOBS_CONTEXT"); return new Response( JSON.stringify({ message: `Hello ${name} from Netlify Edge!`, location: `${city}, ${country}`, runtime: "Deno", timestamp: new Date().toISOString() }), { headers: { "content-type": "application/json", "cache-control": "public, max-age=3600" } } ); };

認証システムの実装

Edge Function を使用した認証システムは、グローバルに低レイテンシでユーザー認証を提供できます。

JWT認証の実装

// Cloudflare Workers: JWT認証ミドルウェア import jwt from '@tsndr/cloudflare-worker-jwt'; interface AuthEnv { JWT_SECRET: string; USERS_KV: KVNamespace; } export async function authenticate( request: Request, env: AuthEnv ): Promise<{ user?: any; error?: string }> { const authHeader = request.headers.get('Authorization'); if (!authHeader?.startsWith('Bearer ')) { return { error: 'Missing authorization header' }; } const token = authHeader.substring(7); try { // トークンの検証 const isValid = await jwt.verify(token, env.JWT_SECRET); if (!isValid) { return { error: 'Invalid token' }; } // デコードしてユーザー情報を取得 const { payload } = jwt.decode(token); // ユーザー情報をKVから取得(キャッシュ) const userKey = `user:${payload.sub}`; const cachedUser = await env.USERS_KV.get(userKey); if (cachedUser) { return { user: JSON.parse(cachedUser) }; } // キャッシュミスの場合はデータベースから取得 // (実際の実装では外部APIを呼び出す) const user = await fetchUserFromDatabase(payload.sub); // KVにキャッシュ(5分間) await env.USERS_KV.put( userKey, JSON.stringify(user), { expirationTtl: 300 } ); return { user }; } catch (error) { return { error: 'Token verification failed' }; } }
// Vercel Edge Functions: OAuth統合 import { SignJWT, jwtVerify } from 'jose'; import { NextRequest } from 'next/server'; const secret = new TextEncoder().encode( process.env.JWT_SECRET! ); export async function verifyAuth(request: NextRequest) { const token = request.cookies.get('auth-token')?.value; if (!token) { return { success: false, error: 'No token found' }; } try { const { payload } = await jwtVerify(token, secret); // ペイロードの検証 if (!payload.sub || !payload.exp) { return { success: false, error: 'Invalid token payload' }; } // 有効期限のチェック const now = Math.floor(Date.now() / 1000); if (payload.exp < now) { return { success: false, error: 'Token expired' }; } // 地理的制限のチェック(オプション) const allowedCountries = ['JP', 'US', 'GB']; const userCountry = request.geo?.country; if (userCountry && !allowedCountries.includes(userCountry)) { return { success: false, error: 'Access denied from your location' }; } return { success: true, user: { id: payload.sub, email: payload.email, role: payload.role } }; } catch (error) { return { success: false, error: 'Token verification failed' }; } } // トークン生成 export async function generateToken(user: any) { const token = await new SignJWT({ sub: user.id, email: user.email, role: user.role }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('24h') .sign(secret); return token; }
// リフレッシュトークンの処理 interface RefreshTokenEnv { JWT_SECRET: string; REFRESH_SECRET: string; TOKENS_KV: KVNamespace; } export async function refreshToken( request: Request, env: RefreshTokenEnv ): Promise<Response> { const refreshToken = request.headers.get('X-Refresh-Token'); if (!refreshToken) { return new Response( JSON.stringify({ error: 'Refresh token required' }), { status: 401 } ); } try { // リフレッシュトークンの検証 const isValid = await jwt.verify( refreshToken, env.REFRESH_SECRET ); if (!isValid) { return new Response( JSON.stringify({ error: 'Invalid refresh token' }), { status: 401 } ); } const { payload } = jwt.decode(refreshToken); // トークンがブラックリストにないか確認 const blacklisted = await env.TOKENS_KV.get( `blacklist:${refreshToken}` ); if (blacklisted) { return new Response( JSON.stringify({ error: 'Token has been revoked' }), { status: 401 } ); } // 新しいアクセストークンを生成 const newAccessToken = await jwt.sign({ sub: payload.sub, email: payload.email, role: payload.role, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1時間 }, env.JWT_SECRET); // 新しいリフレッシュトークンも生成(ローテーション) const newRefreshToken = await jwt.sign({ sub: payload.sub, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 30) // 30日 }, env.REFRESH_SECRET); // 古いリフレッシュトークンをブラックリストに追加 await env.TOKENS_KV.put( `blacklist:${refreshToken}`, 'revoked', { expirationTtl: 60 * 60 * 24 * 30 } ); return new Response( JSON.stringify({ accessToken: newAccessToken, refreshToken: newRefreshToken, expiresIn: 3600 }), { headers: { 'Content-Type': 'application/json' } } ); } catch (error) { return new Response( JSON.stringify({ error: 'Token refresh failed' }), { status: 500 } ); } }

API Gatewayとしての活用

Edge Function を API Gateway として使用することで、認証、レート制限、ルーティング、キャッシングなどを統一的に管理できます。

API Gateway実装パターン

Edge Function API Gatewayアーキテクチャ

チャートを読み込み中...

実装例:マルチサービスAPI Gateway

// Cloudflare Workers: API Gateway実装
interface GatewayEnv {
  RATE_LIMIT_KV: KVNamespace;
  CACHE_KV: KVNamespace;
  API_KEYS: string;
  BACKEND_URLS: string;
}

interface BackendConfig {
  [key: string]: {
    url: string;
    timeout: number;
    cache?: {
      ttl: number;
      key: (request: Request) => string;
    };
  };
}

export default {
  async fetch(
    request: Request,
    env: GatewayEnv,
    ctx: ExecutionContext
  ): Promise<Response> {
    // CORSヘッダーの設定
    if (request.method === 'OPTIONS') {
      return handleCORS();
    }
    
    // レート制限のチェック
    const rateLimitResult = await checkRateLimit(
      request, 
      env.RATE_LIMIT_KV
    );
    
    if (!rateLimitResult.allowed) {
      return new Response('Too Many Requests', { 
        status: 429,
        headers: {
          'Retry-After': rateLimitResult.retryAfter.toString()
        }
      });
    }
    
    // APIキー認証
    const apiKey = request.headers.get('X-API-Key');
    if (!apiKey || !env.API_KEYS.includes(apiKey)) {
      return new Response('Unauthorized', { status: 401 });
    }
    
    // バックエンドの設定
    const backends: BackendConfig = JSON.parse(env.BACKEND_URLS);
    const url = new URL(request.url);
    const [, service, ...pathParts] = url.pathname.split('/');
    
    const backend = backends[service];
    if (!backend) {
      return new Response('Service not found', { status: 404 });
    }
    
    // キャッシュチェック
    if (request.method === 'GET' && backend.cache) {
      const cacheKey = backend.cache.key(request);
      const cached = await env.CACHE_KV.get(cacheKey);
      
      if (cached) {
        return new Response(cached, {
          headers: {
            'Content-Type': 'application/json',
            'X-Cache': 'HIT'
          }
        });
      }
    }
    
    // バックエンドへのリクエスト
    const backendUrl = `${backend.url}/${pathParts.join('/')}${url.search}`;
    const backendRequest = new Request(backendUrl, {
      method: request.method,
      headers: request.headers,
      body: request.body
    });
    
    try {
      const response = await fetch(backendRequest, {
        signal: AbortSignal.timeout(backend.timeout)
      });
      
      // レスポンスの加工
      const data = await response.json();
      const enrichedData = {
        ...data,
        _metadata: {
          service,
          timestamp: new Date().toISOString(),
          region: request.cf?.colo
        }
      };
      
      const responseBody = JSON.stringify(enrichedData);
      
      // キャッシュの保存
      if (request.method === 'GET' && backend.cache && response.ok) {
        ctx.waitUntil(
          env.CACHE_KV.put(
            backend.cache.key(request),
            responseBody,
            { expirationTtl: backend.cache.ttl }
          )
        );
      }
      
      return new Response(responseBody, {
        status: response.status,
        headers: {
          'Content-Type': 'application/json',
          'X-Cache': 'MISS'
        }
      });
    } catch (error) {
      return new Response(
        JSON.stringify({ 
          error: 'Backend service unavailable',
          service
        }),
        { status: 503 }
      );
    }
  }
};

// レート制限の実装
async function checkRateLimit(
  request: Request,
  kv: KVNamespace
): Promise<{ allowed: boolean; retryAfter: number }> {
  const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
  const key = `rate_limit:${ip}`;
  const limit = 100; // 1分あたり100リクエスト
  const window = 60; // 60秒
  
  const current = await kv.get(key);
  const count = current ? parseInt(current) : 0;
  
  if (count >= limit) {
    return { allowed: false, retryAfter: window };
  }
  
  await kv.put(key, (count + 1).toString(), {
    expirationTtl: window
  });
  
  return { allowed: true, retryAfter: 0 };
}

パーソナライゼーションとA/Bテスト

Edge Function の地理的分散と低レイテンシを活用して、リアルタイムパーソナライゼーションを実現できます。

地理的パーソナライゼーション

// Vercel Edge Functions: 地域別コンテンツ配信
import { NextRequest, NextResponse } from 'next/server';

export async function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US';
  const city = request.geo?.city || 'Unknown';
  const language = request.headers.get('accept-language')?.split(',')[0] || 'en';
  
  // 国別の設定
  const countryConfig = {
    JP: {
      currency: 'JPY',
      locale: 'ja-JP',
      timezone: 'Asia/Tokyo',
      features: ['qr-payment', 'line-login']
    },
    US: {
      currency: 'USD',
      locale: 'en-US',
      timezone: 'America/New_York',
      features: ['apple-pay', 'google-pay']
    },
    GB: {
      currency: 'GBP',
      locale: 'en-GB',
      timezone: 'Europe/London',
      features: ['open-banking']
    }
  };
  
  const config = countryConfig[country] || countryConfig.US;
  
  // レスポンスヘッダーに地域情報を追加
  const response = NextResponse.next();
  response.headers.set('X-User-Country', country);
  response.headers.set('X-User-City', city);
  response.headers.set('X-User-Currency', config.currency);
  response.headers.set('X-User-Locale', config.locale);
  
  // A/Bテストの割り当て
  const abTestGroup = await assignABTestGroup(request);
  response.headers.set('X-AB-Test-Group', abTestGroup);
  
  // 地域制限のチェック
  const restrictedCountries = ['CN', 'RU', 'KP'];
  if (restrictedCountries.includes(country)) {
    return NextResponse.redirect(new URL('/restricted', request.url));
  }
  
  return response;
}

// A/Bテストグループの割り当て
async function assignABTestGroup(request: NextRequest): Promise<string> {
  const cookie = request.cookies.get('ab-test-group');
  
  if (cookie) {
    return cookie.value;
  }
  
  // ユーザーIDまたはIPアドレスでハッシュを生成
  const userId = request.cookies.get('user-id')?.value || 
                 request.ip || 
                 'anonymous';
  
  const encoder = new TextEncoder();
  const data = encoder.encode(userId);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  
  // ハッシュ値に基づいてグループを決定(50/50分割)
  const group = parseInt(hashHex.substring(0, 8), 16) % 2 === 0 ? 'A' : 'B';
  
  return group;
}

export const config = {
  matcher: ['/((?!api|_next/static|favicon.ico).*)']
};

動的コンテンツの最適化

// Cloudflare Workers: 画像の動的最適化 export default { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const imageURL = url.searchParams.get('url'); if (!imageURL) { return new Response('Image URL required', { status: 400 }); } // クライアントの情報を取得 const accept = request.headers.get('Accept') || ''; const saveData = request.headers.get('Save-Data') === 'on'; const connection = request.headers.get('Connection'); // 画像フォーマットの決定 let format = 'jpeg'; if (accept.includes('image/avif')) { format = 'avif'; } else if (accept.includes('image/webp')) { format = 'webp'; } // 画質の決定 let quality = 85; if (saveData || connection === 'slow-2g') { quality = 60; } // デバイスタイプの検出 const userAgent = request.headers.get('User-Agent') || ''; const isMobile = /Mobile|Android|iPhone/i.test(userAgent); // サイズの決定 const width = isMobile ? 750 : 1920; // Cloudflare Image Resizingを使用 const resizeOptions = { cf: { image: { format, quality, width, fit: 'scale-down', metadata: 'none', sharpen: 1.0 } } }; const imageRequest = new Request(imageURL, { headers: request.headers, ...resizeOptions }); const response = await fetch(imageRequest); // キャッシュヘッダーの設定 const headers = new Headers(response.headers); headers.set('Cache-Control', 'public, max-age=31536000'); headers.set('Vary', 'Accept, Save-Data'); return new Response(response.body, { status: response.status, headers }); } };
// コンテンツの動的変換 interface ContentTransformEnv { TRANSLATIONS_KV: KVNamespace; CONTENT_KV: KVNamespace; } export default { async fetch( request: Request, env: ContentTransformEnv ): Promise<Response> { const url = new URL(request.url); const contentId = url.pathname.substring(1); const targetLang = request.headers.get('Accept-Language') ?.split(',')[0] || 'en'; // オリジナルコンテンツの取得 const content = await env.CONTENT_KV.get(contentId); if (!content) { return new Response('Content not found', { status: 404 }); } const data = JSON.parse(content); // 翻訳済みかチェック const translationKey = `${contentId}:${targetLang}`; const cached = await env.TRANSLATIONS_KV.get(translationKey); if (cached) { return new Response(cached, { headers: { 'Content-Type': 'application/json', 'Content-Language': targetLang, 'X-Translation-Cache': 'HIT' } }); } // リアルタイム翻訳(実際のAPIは省略) const translated = await translateContent(data, targetLang); // パーソナライズされたコンテンツの生成 const personalized = { ...translated, recommendations: await getPersonalizedRecommendations( request, contentId ), localizedPricing: await getLocalizedPricing( request.cf?.country || 'US' ) }; const response = JSON.stringify(personalized); // 翻訳結果をキャッシュ await env.TRANSLATIONS_KV.put( translationKey, response, { expirationTtl: 3600 } ); return new Response(response, { headers: { 'Content-Type': 'application/json', 'Content-Language': targetLang, 'X-Translation-Cache': 'MISS' } }); } };
// リアルタイム分析とメトリクス収集 interface AnalyticsEnv { ANALYTICS_KV: KVNamespace; CLICKHOUSE_URL: string; CLICKHOUSE_TOKEN: string; } export default { async fetch( request: Request, env: AnalyticsEnv, ctx: ExecutionContext ): Promise<Response> { const startTime = Date.now(); // リクエスト情報の収集 const analyticsData = { timestamp: new Date().toISOString(), url: request.url, method: request.method, country: request.cf?.country || 'Unknown', city: request.cf?.city || 'Unknown', userAgent: request.headers.get('User-Agent') || '', referer: request.headers.get('Referer') || '', ip: request.headers.get('CF-Connecting-IP') || '', // パフォーマンスメトリクス cfRay: request.headers.get('CF-Ray') || '', colo: request.cf?.colo || '' }; // メインレスポンスの処理 const response = await handleMainRequest(request); // レスポンスメトリクスの追加 analyticsData.statusCode = response.status; analyticsData.responseTime = Date.now() - startTime; analyticsData.contentLength = response.headers.get('Content-Length') || '0'; // バックグラウンドでの分析データ送信 ctx.waitUntil( sendAnalytics(analyticsData, env) ); // リアルタイムダッシュボード用の集計 ctx.waitUntil( updateRealtimeMetrics(analyticsData, env.ANALYTICS_KV) ); return response; } }; async function sendAnalytics( data: any, env: AnalyticsEnv ): Promise<void> { // ClickHouseへのバッチ送信 const batch = [data]; await fetch(env.CLICKHOUSE_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${env.CLICKHOUSE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: `INSERT INTO analytics FORMAT JSONEachRow`, data: batch.map(item => JSON.stringify(item)).join('\n') }) }); } async function updateRealtimeMetrics( data: any, kv: KVNamespace ): Promise<void> { const minute = new Date().toISOString().substring(0, 16); const key = `metrics:${minute}`; // 現在の分のメトリクスを取得 const current = await kv.get(key); const metrics = current ? JSON.parse(current) : { requests: 0, totalResponseTime: 0, statusCodes: {}, countries: {} }; // メトリクスの更新 metrics.requests++; metrics.totalResponseTime += data.responseTime; metrics.statusCodes[data.statusCode] = (metrics.statusCodes[data.statusCode] || 0) + 1; metrics.countries[data.country] = (metrics.countries[data.country] || 0) + 1; // 保存(TTL: 1時間) await kv.put(key, JSON.stringify(metrics), { expirationTtl: 3600 }); }

パフォーマンス最適化戦略

キャッシング戦略

Edge Functionでのキャッシング戦略
戦略 説明 ユースケース 実装例
エッジキャッシュ CDNレベルでの静的アセットキャッシュ イメージ、CSS、JS Cache-Control: public, max-age=31536000
KVキャッシュ Key-Valueストアでの動的データキャッシュ APIレスポンス、セッション env.KV.put(key, value, {expirationTtl: 300})
ブラウザキャッシュ クライアントサイドキャッシュ パーソナライズデータ Cache-Control: private, max-age=3600
Stale-While-Revalidate キャッシュ更新中も古いデータを配信 ニュース、商品情報 Cache-Control: s-maxage=60, stale-while-revalidate=300

パフォーマンスベストプラクティス

パフォーマンス最適化のポイント

  1. コールドスタートの最小化: Vercel Fluid Compute の活用
  2. バンドルサイズの削減: 必要最小限のライブラリのみ使用
  3. 非同期処理の活用: waitUntil で重い処理をバックグラウンド実行
  4. 地理的最適化: ユーザーに最も近いエッジで処理
  5. 効率的なデータ取得: GraphQL やデータローダーパターンの使用

実装例:最適化されたAPIエンドポイント

// パフォーマンス最適化されたEdge Function
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // 1. 早期リターンの活用
    const cached = await checkCache(request, env);
    if (cached) return cached;
    
    // 2. 並列処理の実装
    const [userData, productData, recommendations] = await Promise.all([
      fetchUserData(request, env),
      fetchProductData(request, env),
      fetchRecommendations(request, env)
    ]);
    
    // 3. ストリーミングレスポンス
    const { readable, writable } = new TransformStream();
    const writer = writable.getWriter();
    
    // ヘッダーを即座に送信
    writer.write(new TextEncoder().encode('{"status":"ok","data":['));
    
    // データを順次ストリーミング
    for await (const chunk of processDataStream(userData, productData)) {
      writer.write(new TextEncoder().encode(JSON.stringify(chunk) + ','));
    }
    
    writer.write(new TextEncoder().encode(']}'));
    writer.close();
    
    return new Response(readable, {
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 's-maxage=10, stale-while-revalidate=60'
      }
    });
  }
};

コスト管理とスケーリング

コスト比較(2025年版)

Edge Functionプロバイダーのコスト比較
プロバイダー 無料枠 追加コスト 含まれるサービス
Cloudflare Workers 100,000リクエスト/日 $5/月 + $0.50/百万リクエスト KV, Durable Objects, R2
Vercel Edge Functions 100,000リクエスト/月 $20/月 + $2/百万リクエスト Analytics, Monitoring
Netlify Edge Functions 125,000リクエスト/月 $19/月 + $2/百万リクエスト Netlify Functions, Forms
AWS Lambda@Edge なし $0.60/百万リクエスト + $0.00005125/GB-秒 CloudFront統合

スケーリング戦略

// 自動スケーリングとリソース管理
interface ScalingConfig {
  maxConcurrency: number;
  timeoutMs: number;
  memoryMB: number;
}

export class EdgeFunctionScaler {
  private config: ScalingConfig;
  private metrics: MetricsCollector;
  
  constructor(config: ScalingConfig) {
    this.config = config;
    this.metrics = new MetricsCollector();
  }
  
  async handleRequest(request: Request): Promise<Response> {
    // リクエストの分類
    const requestType = this.classifyRequest(request);
    
    // 優先度に基づくリソース割り当て
    switch (requestType) {
      case 'critical':
        return this.handleCritical(request);
      case 'normal':
        return this.handleNormal(request);
      case 'batch':
        return this.handleBatch(request);
      default:
        return new Response('Bad Request', { status: 400 });
    }
  }
  
  private async handleCritical(request: Request): Promise<Response> {
    // クリティカルなリクエストは即座に処理
    const timeout = Math.min(this.config.timeoutMs, 5000);
    return this.processWithTimeout(request, timeout);
  }
  
  private async handleBatch(request: Request): Promise<Response> {
    // バッチ処理はキューに追加
    const batchId = crypto.randomUUID();
    await this.queueBatchJob(request, batchId);
    
    return new Response(
      JSON.stringify({ 
        batchId, 
        status: 'queued',
        estimatedTime: '30s'
      }),
      { status: 202 }
    );
  }
}

実践例:グローバル対応Webアプリケーション

マルチリージョンEコマースサイト

完全な Edge-first アーキテクチャを採用した E コマースサイトの実装例を紹介します。

// Edge Function: グローバルEコマースゲートウェイ
interface EcommerceEnv {
  PRODUCTS_KV: KVNamespace;
  INVENTORY_API: string;
  PAYMENT_API: string;
  SHIPPING_API: string;
}

export default {
  async fetch(
    request: Request,
    env: EcommerceEnv,
    ctx: ExecutionContext
  ): Promise<Response> {
    const url = new URL(request.url);
    const router = new Router();
    
    // 商品一覧(地域別価格対応)
    router.get('/api/products', async (request) => {
      const country = request.cf?.country || 'US';
      const currency = getCurrency(country);
      
      // KVから商品データを取得
      const productsKey = `products:${country}`;
      let products = await env.PRODUCTS_KV.get(productsKey);
      
      if (!products) {
        // オリジンから取得して地域別に最適化
        const baseProducts = await fetchProductsFromOrigin(env);
        products = await localizeProducts(
          baseProducts, 
          country, 
          currency
        );
        
        // KVにキャッシュ(1時間)
        ctx.waitUntil(
          env.PRODUCTS_KV.put(productsKey, products, {
            expirationTtl: 3600
          })
        );
      }
      
      return new Response(products, {
        headers: {
          'Content-Type': 'application/json',
          'Cache-Control': 's-maxage=300'
        }
      });
    });
    
    // 在庫確認(リアルタイム)
    router.get('/api/inventory/:productId', async (request, { params }) => {
      const { productId } = params;
      const location = request.cf?.city || 'Unknown';
      
      // 最寄りの倉庫から在庫を確認
      const inventory = await checkInventory(
        productId, 
        location,
        env.INVENTORY_API
      );
      
      return new Response(
        JSON.stringify({
          productId,
          available: inventory.quantity > 0,
          quantity: inventory.quantity,
          warehouse: inventory.warehouse,
          estimatedDelivery: inventory.estimatedDelivery
        }),
        {
          headers: {
            'Content-Type': 'application/json',
            'Cache-Control': 'no-cache'
          }
        }
      );
    });
    
    // チェックアウト処理
    router.post('/api/checkout', async (request) => {
      const order = await request.json();
      const country = request.cf?.country || 'US';
      
      // 並列処理で各種検証
      const [
        inventoryCheck,
        fraudCheck,
        shippingQuote,
        taxCalculation
      ] = await Promise.all([
        verifyInventory(order.items, env),
        checkFraud(order, request),
        calculateShipping(order, country, env),
        calculateTax(order, country)
      ]);
      
      if (!inventoryCheck.success) {
        return new Response(
          JSON.stringify({ 
            error: 'Some items are out of stock',
            items: inventoryCheck.unavailable 
          }),
          { status: 400 }
        );
      }
      
      if (fraudCheck.risk > 0.8) {
        return new Response(
          JSON.stringify({ 
            error: 'Transaction flagged for review' 
          }),
          { status: 403 }
        );
      }
      
      // 支払い処理
      const payment = await processPayment({
        ...order,
        shipping: shippingQuote,
        tax: taxCalculation,
        currency: getCurrency(country)
      }, env.PAYMENT_API);
      
      if (payment.success) {
        // 注文確定処理をバックグラウンドで
        ctx.waitUntil(
          finalizeOrder(order, payment, env)
        );
        
        return new Response(
          JSON.stringify({
            orderId: payment.orderId,
            status: 'confirmed',
            estimatedDelivery: shippingQuote.estimatedDate
          }),
          { status: 200 }
        );
      }
      
      return new Response(
        JSON.stringify({ 
          error: 'Payment failed',
          reason: payment.error 
        }),
        { status: 400 }
      );
    });
    
    return router.handle(request);
  }
};

// 地域別通貨マッピング
function getCurrency(country: string): string {
  const currencyMap: Record<string, string> = {
    JP: 'JPY',
    US: 'USD',
    GB: 'GBP',
    EU: 'EUR',
    CN: 'CNY',
    KR: 'KRW',
    AU: 'AUD',
    CA: 'CAD'
  };
  
  return currencyMap[country] || 'USD';
}

// 商品のローカライゼーション
async function localizeProducts(
  products: any[],
  country: string,
  currency: string
): Promise<string> {
  const exchangeRate = await getExchangeRate('USD', currency);
  
  const localized = products.map(product => ({
    ...product,
    price: {
      amount: Math.round(product.price * exchangeRate),
      currency,
      formatted: formatPrice(
        product.price * exchangeRate, 
        currency
      )
    },
    availability: getLocalAvailability(product, country),
    shipping: getShippingOptions(product, country)
  }));
  
  return JSON.stringify(localized);
}

パフォーマンス結果

TTFB改善率 85 %
コスト削減 40 %
可用性(99.95%) 95 %

まとめとベストプラクティス

Edge Function選択フローチャート

Edge Functionプラットフォーム選択ガイド

チャートを読み込み中...

ベストプラクティスチェックリスト

Edge Function実装のベストプラクティス

セキュリティ

  • 環境変数でシークレット管理
  • CORS ヘッダーの適切な設定
  • 入力検証とサニタイゼーション

パフォーマンス

  • キャッシング戦略の実装
  • 並列処理の活用
  • ストリーミングレスポンス

可用性

  • エラーハンドリング
  • フォールバック処理
  • ヘルスチェックエンドポイント

監視とログ

  • 構造化ログの実装
  • メトリクス収集
  • エラートラッキング

開発プロセス

  • ローカル開発環境の整備
  • CI/CD パイプライン
  • ステージング環境でのテスト

今後の展望

2025 年の Edge Function は、単なる CDN エッジでの処理を超えて、完全なアプリケーションプラットフォームへと進化しています。特に注目すべきトレンドは:

  1. コンテナサポート: Cloudflareの 2025 年 6 月のコンテナ対応により、より複雑なワークロードの実行が可能に
  2. AI/ML統合: エッジでの推論処理によるリアルタイムパーソナライゼーション
  3. WebAssembly: より多様な言語での Edge Function 開発
  4. データベース統合: エッジネイティブなデータストレージソリューション

2025 年までに、全データの 75%がエッジで生成・処理されるようになり、Edge Function は現代の Web アーキテクチャにおいて不可欠な要素となる。

ガートナー予測 調査レポート

Edge Function を活用することで、グローバルスケールでの高速な Web アプリケーション構築が可能になります。適切なプラットフォーム選択と実装パターンの理解により、ユーザー体験の向上とコスト最適化を同時に実現できるでしょう。

Rinaのプロフィール画像

Rina

Daily Hack 編集長

フルスタックエンジニアとして10年以上の経験を持つ。 大手IT企業やスタートアップでの開発経験を活かし、 実践的で即効性のある技術情報を日々発信中。 特にWeb開発、クラウド技術、AI活用に精通。

この記事は役に立ちましたか?

あなたのフィードバックが記事の改善に役立ちます

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。