S
Shineos Tech Blog
Vercel/Cloudflare Pagesを活用したエッジコンピューティング - 高速・低遅延なWebアプリの実現

Vercel/Cloudflare Pagesを活用したエッジコンピューティング - 高速・低遅延なWebアプリの実現

| Shineos Dev Team
Share:

はじめに

Webアプリケーションのパフォーマンスは、ユーザー体験を左右する重要な要素です。特にグローバル展開するサービスでは、地理的に離れたユーザーへの応答速度が課題となります。エッジコンピューティングは、コンピューティングリソースをユーザーに近い場所に配置することで、この課題を解決する技術です。

本記事では、VercelとCloudflare Pagesを活用したエッジコンピューティングの実装方法を、実務で使える実践的な手法とともに解説します。

エッジコンピューティングとは?

エッジコンピューティングとは、データ処理をクラウドのデータセンター(中央)ではなく、ユーザーに近い「エッジ」のロケーションで実行する技術です。従来のCDN(Content Delivery Network)がキャッシュされた静的コンテンツを配信するのに対し、エッジコンピューティングでは動的な処理も実行できます。

エッジコンピューティングの主な特徴

特徴説明メリット
低レイテンシユーザーに物理的に近い場所で処理応答時間の大幅な短縮
グローバル分散世界中のエッジロケーションで実行地域による性能差の最小化
動的処理APIリクエストや認証処理をエッジで実行バックエンドの負荷軽減
自動スケーリングトラフィックに応じて自動拡張インフラ管理の簡素化

まとめ

本記事で解説したエッジコンピューティングの重要なポイントは以下の通りです:

エッジコンピューティングの活用場面

  • 静的サイト生成(SSG)とエッジでの動的処理の組み合わせ
  • パーソナライゼーション、A/Bテスト、認証処理のエッジ実装
  • CDNとEdge Functionsの連携による最適化

Vercel vs Cloudflare Pages

  • Vercel:Next.jsとの統合が強力、開発体験に優れる
  • Cloudflare Pages:グローバルネットワークが広大、Workers連携が柔軟

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

  • Edge Functionsの実行時間を最小化(Cold Start対策)
  • 適切なキャッシング戦略の実装
  • 地域ごとのパフォーマンス監視

VercelとCloudflare Pagesの比較

VercelとCloudflare Pagesは、どちらもエッジコンピューティングに対応したホスティングプラットフォームですが、特徴が異なります。

比較表

項目VercelCloudflare Pages
Edge Networkグローバル 110+ ロケーション世界 275+ 都市以上
Edge FunctionsVercel Edge Functions(V8)Cloudflare Workers(V8)
フレームワーク対応Next.js、SvelteKit等多数のフレームワークに対応
Cold Start比較的速い非常に速い(0ms Cold Start)
実行時間制限30秒(Hobby)、25秒(Edge)50ms(無料)、無制限(有料)
料金$20/月〜(Pro)$5/月〜(有料プラン)
開発体験優れたDX、Next.js統合Workers連携、KVストレージ

従来のアーキテクチャとエッジコンピューティングの比較

Vercel Edge Functionsの実装

VercelのEdge Functionsは、Vercelのグローバルエッジネットワークで実行されるサーバーレス関数です。

基本的な実装例

// pages/api/edge-hello.ts
import { NextRequest, NextResponse } from 'next/server';

export const config = {
  runtime: 'edge',
};

export default async function handler(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const name = searchParams.get('name') || 'World';
  
  return NextResponse.json({
    message: `Hello, ${name}!`,
    timestamp: new Date().toISOString(),
    location: req.geo?.city || 'Unknown',
  });
}

パーソナライゼーションの実装

エッジで位置情報を取得し、ユーザーに最適化されたコンテンツを提供します。

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const { geo } = request;
  const country = geo?.country || 'US';
  const city = geo?.city || 'Unknown';
  
  // 位置情報に基づいてレスポンスをカスタマイズ
  const response = NextResponse.next();
  
  // カスタムヘッダーを追加
  response.headers.set('x-user-country', country);
  response.headers.set('x-user-city', city);
  
  // 地域別のリダイレクト(日本からのアクセスを日本語ページへ)
  if (country === 'JP' && !request.nextUrl.pathname.startsWith('/ja')) {
    return NextResponse.redirect(new URL(`/ja${request.nextUrl.pathname}`, request.url));
  }
  
  return response;
}

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

A/Bテストの実装

エッジでA/Bテストを実装し、ユーザーごとに異なるバージョンを配信します。

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

const COOKIE_NAME = 'ab-test-variant';
const VARIANTS = ['control', 'variant-a', 'variant-b'];

export function middleware(request: NextRequest) {
  // 既存のバリアントをクッキーから取得
  let variant = request.cookies.get(COOKIE_NAME)?.value;
  
  // バリアントが未設定の場合はランダムに割り当て
  if (!variant || !VARIANTS.includes(variant)) {
    variant = VARIANTS[Math.floor(Math.random() * VARIANTS.length)];
  }
  
  // バリアントに応じてリライト
  const url = request.nextUrl.clone();
  
  if (variant !== 'control' && url.pathname === '/') {
    url.pathname = `/ab-test/${variant}`;
  }
  
  const response = NextResponse.rewrite(url);
  
  // クッキーにバリアントを保存(7日間)
  response.cookies.set(COOKIE_NAME, variant, {
    maxAge: 60 * 60 * 24 * 7,
    path: '/',
  });
  
  return response;
}

Cloudflare Pages + Workersの実装

Cloudflare PagesはCloudflare Workersと統合されており、強力なエッジ機能を提供します。

Cloudflare Pages Functionsの基本

// functions/api/hello.ts
export async function onRequest(context) {
  const { request, env } = context;
  const url = new URL(request.url);
  const name = url.searchParams.get('name') || 'World';
  
  return new Response(
    JSON.stringify({
      message: `Hello, ${name}!`,
      timestamp: new Date().toISOString(),
      country: request.cf?.country || 'Unknown',
      colo: request.cf?.colo || 'Unknown', // Cloudflareデータセンターのコード
    }),
    {
      headers: {
        'content-type': 'application/json',
      },
    }
  );
}

KV(Key-Value)ストレージの活用

Cloudflare Workers KVを使用して、エッジでデータを保存・取得します。

// functions/api/counter.ts
export async function onRequestGet(context) {
  const { env } = context;
  
  // KVから現在のカウントを取得
  const count = await env.COUNTER_KV.get('page_views') || '0';
  
  return new Response(
    JSON.stringify({ views: parseInt(count) }),
    {
      headers: { 'content-type': 'application/json' },
    }
  );
}

export async function onRequestPost(context) {
  const { env } = context;
  
  // カウントをインクリメント
  const count = await env.COUNTER_KV.get('page_views') || '0';
  const newCount = parseInt(count) + 1;
  
  await env.COUNTER_KV.put('page_views', newCount.toString());
  
  return new Response(
    JSON.stringify({ views: newCount }),
    {
      headers: { 'content-type': 'application/json' },
    }
  );
}

Cloudflare PagesとWorkersのアーキテクチャ

エッジでの認証処理

// functions/_middleware.ts
import { verifyJWT } from './utils/jwt';

export async function onRequest(context) {
  const { request, next } = context;
  const url = new URL(request.url);
  
  // 認証が不要なパス
  const publicPaths = ['/login', '/signup', '/api/public'];
  if (publicPaths.some(path => url.pathname.startsWith(path))) {
    return next();
  }
  
  // JWTトークンの検証
  const authHeader = request.headers.get('Authorization');
  const token = authHeader?.replace('Bearer ', '');
  
  if (!token) {
    return new Response('Unauthorized', { status: 401 });
  }
  
  try {
    const payload = await verifyJWT(token, context.env.JWT_SECRET);
    
    // ユーザー情報をヘッダーに追加
    const response = await next();
    response.headers.set('x-user-id', payload.sub);
    
    return response;
  } catch (error) {
    return new Response('Invalid token', { status: 401 });
  }
}

パフォーマンス最適化のテクニック

エッジコンピューティングの効果を最大化するための最適化手法を紹介します。

キャッシング戦略

// Vercel Edge Functionsでのキャッシング
import { NextRequest, NextResponse } from 'next/server';

export const config = {
  runtime: 'edge',
};

export default async function handler(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const productId = searchParams.get('id');
  
  // APIからデータを取得
  const product = await fetchProductFromAPI(productId);
  
  const response = NextResponse.json(product);
  
  // キャッシュヘッダーを設定(1時間キャッシュ、バックグラウンドで再検証)
  response.headers.set(
    'Cache-Control',
    's-maxage=3600, stale-while-revalidate=86400'
  );
  
  return response;
}

async function fetchProductFromAPI(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    // Fetch APIのキャッシュオプション
    next: { revalidate: 3600 }, // Next.js 13+の場合
  });
  
  return res.json();
}

ストリーミングレスポンス

大きなレスポンスをストリーミングで送信し、初回バイトまでの時間(TTFB)を改善します。

// Cloudflare Workersでのストリーミング
export async function onRequest(context) {
  const { readable, writable } = new TransformStream();
  const writer = writable.getWriter();
  const encoder = new TextEncoder();
  
  // バックグラウンドでデータを送信
  (async () => {
    try {
      // 最初のチャンクをすぐに送信
      await writer.write(encoder.encode('{"status":"processing","items":['));
      
      // データを段階的に取得して送信
      for (let i = 0; i < 10; i++) {
        const item = await fetchItem(i);
        const prefix = i > 0 ? ',' : '';
        await writer.write(encoder.encode(`${prefix}${JSON.stringify(item)}`));
        
        // 少し待機(リアルなストリーミングをシミュレート)
        await new Promise(resolve => setTimeout(resolve, 100));
      }
      
      await writer.write(encoder.encode(']}'));
    } catch (error) {
      console.error('Streaming error:', error);
    } finally {
      await writer.close();
    }
  })();
  
  return new Response(readable, {
    headers: {
      'content-type': 'application/json',
      'transfer-encoding': 'chunked',
    },
  });
}

リージョン別のフォールバック

特定のリージョンでエラーが発生した場合のフォールバック処理を実装します。

// Vercel Edge Functionsでのフォールバック
export default async function handler(req: NextRequest) {
  const { geo } = req;
  const region = geo?.region || 'default';
  
  // リージョン別のAPIエンドポイント
  const endpoints = {
    'us-east': 'https://api-us-east.example.com',
    'eu-west': 'https://api-eu-west.example.com',
    'ap-northeast': 'https://api-ap-northeast.example.com',
    'default': 'https://api.example.com',
  };
  
  const primaryEndpoint = endpoints[region] || endpoints.default;
  const fallbackEndpoint = endpoints.default;
  
  try {
    // 最初にリージョンのAPIを試行
    const response = await fetch(primaryEndpoint, {
      signal: AbortSignal.timeout(5000), // 5秒でタイムアウト
    });
    
    if (response.ok) {
      return NextResponse.json(await response.json());
    }
    
    throw new Error('Primary endpoint failed');
  } catch (error) {
    // フォールバックAPIを使用
    console.warn(`Falling back from ${primaryEndpoint} to ${fallbackEndpoint}`);
    
    const response = await fetch(fallbackEndpoint);
    return NextResponse.json(await response.json());
  }
}

実装例:Next.js + Vercelでのエッジ最適化

完全な実装例を見てみましょう。

// app/page.tsx (Server Component)
import { headers } from 'next/headers';

export default async function Home() {
  const headersList = headers();
  const country = headersList.get('x-user-country') || 'Unknown';
  const city = headersList.get('x-user-city') || 'Unknown';
  
  return (
    <div>
      <h1>Welcome from {city}, {country}!</h1>
      <ProductList />
    </div>
  );
}

// app/components/ProductList.tsx (Server Component with caching)
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }, // 1時間キャッシュ
  });
  
  return res.json();
}

export default async function ProductList() {
  const products = await getProducts();
  
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// middleware.ts (Edge Runtime)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { geo } = request;
  
  const response = NextResponse.next();
  
  // 位置情報をヘッダーに追加
  response.headers.set('x-user-country', geo?.country || 'Unknown');
  response.headers.set('x-user-city', geo?.city || 'Unknown');
  
  return response;
}

よくある質問

エッジ関数とサーバーレス関数の使い分けはどうすべきか?

エッジ関数は低遅延が求められる処理(認証、パーソナライゼーション、A/Bテスト、リダイレクト)に適しています。一方、重い計算処理、データベースへの複雑なクエリ、長時間実行する処理はサーバーレス関数またはバックエンドサーバーで実行すべきです。一般的な目安として、100ms以内で完了する処理はエッジ関数、それ以上かかる処理は通常のサーバーレス関数が適しています。

Cloudflare WorkersのKVストレージはどのような用途に適しているか?

KVストレージは、読み取りが多く書き込みが少ないデータ(設定情報、ユーザープロファイル、A/Bテストのバリアント、キャッシュ)に最適です。結果整合性モデルのため、厳密なトランザクションが必要な処理(金融取引、在庫管理)には向いていません。また、グローバルに複製されるため、読み取り性能は非常に高速ですが、書き込みは反映に時間がかかる場合があります。

VercelとCloudflare Pagesを併用することは可能か?

技術的には可能ですが、一般的には推奨されません。両プラットフォームは似た機能を提供しており、管理の複雑さが増すためです。ただし、特定のサービス(例:メインサイトをVercelでホスト、特定のマイクロサービスをCloudflare Workersで実装)を分けて運用する戦略は有効な場合があります。重要なのは、チームのスキルセットと各サービスの強みを活かした設計です。

おわりに

エッジコンピューティングは、グローバルなWebアプリケーションのパフォーマンスを大幅に改善する技術です。VercelとCloudflare Pagesは、それぞれ異なる強みを持ち、プロジェクトの特性に応じて選択できます。

本記事で紹介した実装パターンと最適化手法を活用することで、ユーザーに高速で快適な体験を提供できます。エッジ関数の制限事項を理解し、適切な処理をエッジに配置することが成功の鍵です。

私たちShineosでは、最新のエッジコンピューティング技術を活用したWebアプリケーション開発をサポートしています。パフォーマンス最適化でお困りの際は、ぜひご相談ください。

参考リンク