Vercel Edge Functions完全ガイド - グローバル分散APIで超高速レスポンス


Vercel Edge Functionsは、世界中に分散されたエッジネットワークで実行されるサーバーレス関数です。従来のサーバーレス関数(AWS Lambda等)と比べて圧倒的に高速で、グローバルなユーザーに低レイテンシでコンテンツを配信できます。

Edge Functionsとは?

Edge Functionsは、ユーザーに最も近いエッジロケーション(CDNノード)で実行される軽量な関数です。

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

項目Edge Functions従来のサーバーレス
実行場所世界中のエッジ(300+拠点)特定リージョン
コールドスタートほぼゼロ(<1ms)50-500ms
レスポンス時間10-100ms100-1000ms
ランタイムEdge RuntimeNode.js
メモリ制限128MB最大10GB
実行時間制限30秒15分

いつEdge Functionsを使うべきか?

Edge Functions向き:

  • 認証・認可チェック
  • A/Bテスト・機能フラグ
  • リダイレクト・リライト
  • 地理情報ベースのコンテンツ配信
  • Rate Limiting
  • 軽量なAPI(JSONレスポンス等)

Node.js Runtime向き:

  • 重い計算処理
  • ファイルシステムアクセス
  • ネイティブNode.jsモジュール使用
  • 大量のメモリが必要な処理

Next.jsでのEdge Functions

Route HandlerでEdge Runtimeを使用

app/api/hello/route.ts

export const runtime = 'edge';

export async function GET() {
  return Response.json({ message: 'Hello from Edge!' });
}

たったこれだけで、このAPIはEdge Functionsとして実行されます。

リクエスト情報の取得

Edge Functionsでは、リクエストの地理情報やIPアドレスを簡単に取得できます。

export const runtime = 'edge';

export async function GET(request: Request) {
  // 地理情報
  const geo = request.headers.get('x-vercel-ip-country');
  const city = request.headers.get('x-vercel-ip-city');
  const region = request.headers.get('x-vercel-ip-country-region');
  const latitude = request.headers.get('x-vercel-ip-latitude');
  const longitude = request.headers.get('x-vercel-ip-longitude');

  // IPアドレス
  const ip = request.headers.get('x-forwarded-for');

  return Response.json({
    country: geo,
    city: decodeURIComponent(city || ''),
    region,
    latitude,
    longitude,
    ip,
  });
}

TypeScript型定義

types/edge.d.ts

declare global {
  interface Request {
    geo?: {
      city?: string;
      country?: string;
      region?: string;
      latitude?: string;
      longitude?: string;
    };
  }
}

export {};

実用的なユースケース

1. 地理情報ベースのコンテンツ配信

ユーザーの国に応じて異なるコンテンツを返します。

app/api/content/route.ts

export const runtime = 'edge';

export async function GET(request: Request) {
  const country = request.headers.get('x-vercel-ip-country') || 'US';

  const content = {
    JP: {
      title: 'ようこそ',
      message: '日本からのアクセスを検出しました',
      currency: 'JPY',
    },
    US: {
      title: 'Welcome',
      message: 'Access from United States detected',
      currency: 'USD',
    },
    default: {
      title: 'Welcome',
      message: 'Welcome to our service',
      currency: 'USD',
    },
  };

  return Response.json(content[country as keyof typeof content] || content.default);
}

2. A/Bテスト

ユーザーをランダムに振り分けて異なるバージョンを表示します。

middleware.ts

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

export const config = {
  matcher: '/',
};

export function middleware(request: NextRequest) {
  // 既存のバリアントをチェック
  const currentVariant = request.cookies.get('ab-test-variant');

  if (currentVariant) {
    return NextResponse.next();
  }

  // ランダムに振り分け(50/50)
  const variant = Math.random() < 0.5 ? 'A' : 'B';

  const response = NextResponse.rewrite(
    new URL(variant === 'A' ? '/variant-a' : '/variant-b', request.url)
  );

  // Cookieに保存(30日間)
  response.cookies.set('ab-test-variant', variant, {
    maxAge: 60 * 60 * 24 * 30,
  });

  return response;
}

3. 認証チェック

ログインが必要なページへのアクセスをチェックします。

middleware.ts

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

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'],
};

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;

  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('redirect', request.nextUrl.pathname);
    return NextResponse.redirect(loginUrl);
  }

  // トークンの検証(簡易版)
  try {
    // JWTの検証などをここで実行
    const isValid = await verifyToken(token);

    if (!isValid) {
      return NextResponse.redirect(new URL('/login', request.url));
    }

    return NextResponse.next();
  } catch (error) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

async function verifyToken(token: string): Promise<boolean> {
  // 実際のトークン検証ロジック
  return true;
}

4. Rate Limiting

Upstash Redisと組み合わせてレート制限を実装します。

npm install @upstash/ratelimit @upstash/redis

app/api/limited/route.ts

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

export const runtime = 'edge';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

const ratelimit = new Ratelimit({
  redis: redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10リクエスト/10秒
});

export async function GET(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'anonymous';
  const { success, limit, reset, remaining } = await ratelimit.limit(ip);

  if (!success) {
    return Response.json(
      { error: 'Too many requests' },
      {
        status: 429,
        headers: {
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': remaining.toString(),
          'X-RateLimit-Reset': reset.toString(),
        },
      }
    );
  }

  return Response.json({ message: 'Success', remaining });
}

5. カスタムリダイレクト

国や言語に応じたリダイレクトを実装します。

middleware.ts

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

export function middleware(request: NextRequest) {
  const country = request.headers.get('x-vercel-ip-country');
  const { pathname } = request.nextUrl;

  // ルートパスの場合のみ処理
  if (pathname !== '/') {
    return NextResponse.next();
  }

  // 日本からのアクセスは /ja にリダイレクト
  if (country === 'JP') {
    return NextResponse.redirect(new URL('/ja', request.url));
  }

  // アメリカからのアクセスは /en にリダイレクト
  if (country === 'US') {
    return NextResponse.redirect(new URL('/en', request.url));
  }

  return NextResponse.next();
}

6. Bot検出

ボットアクセスを検出してブロックまたは特別な処理を行います。

middleware.ts

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

const BOT_USER_AGENTS = [
  'googlebot',
  'bingbot',
  'slurp',
  'duckduckbot',
  'baiduspider',
  'yandexbot',
  'facebookexternalhit',
  'twitterbot',
];

export function middleware(request: NextRequest) {
  const userAgent = request.headers.get('user-agent')?.toLowerCase() || '';

  const isBot = BOT_USER_AGENTS.some((bot) => userAgent.includes(bot));

  if (isBot) {
    // ボット専用のレスポンスを返す
    return NextResponse.rewrite(new URL('/bot-version', request.url));
  }

  return NextResponse.next();
}

Edge Functionでの外部API呼び出し

Edge RuntimeではNode.jsの一部機能が制限されていますが、fetchは完全にサポートされています。

export const runtime = 'edge';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const city = searchParams.get('city') || 'Tokyo';

  try {
    const response = await fetch(
      `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.WEATHER_API_KEY}`,
      {
        next: { revalidate: 600 }, // 10分間キャッシュ
      }
    );

    const data = await response.json();

    return Response.json({
      city: data.name,
      temperature: data.main.temp,
      weather: data.weather[0].description,
    });
  } catch (error) {
    return Response.json(
      { error: 'Failed to fetch weather data' },
      { status: 500 }
    );
  }
}

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

大きなデータをストリーミングで返すことができます。

export const runtime = 'edge';

export async function GET() {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 10; i++) {
        const message = `データ ${i + 1}\n`;
        controller.enqueue(encoder.encode(message));
        await new Promise((resolve) => setTimeout(resolve, 1000));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'Cache-Control': 'no-cache',
    },
  });
}

パフォーマンス最適化

1. レスポンスのキャッシュ

export const runtime = 'edge';

export async function GET() {
  const data = await fetchData();

  return Response.json(data, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120',
    },
  });
}

2. コードの最小化

Edge Functionsではバンドルサイズが重要です。必要最小限のコードのみをインポートしましょう。

// 悪い例: 全体をインポート
import _ from 'lodash';

// 良い例: 必要な関数のみインポート
import chunk from 'lodash/chunk';

3. 軽量なライブラリを選択

Edge Runtime対応の軽量ライブラリを選びましょう。

  • 日時処理: date-fns(軽量)よりdayjs
  • UUID: nanoid(軽量)
  • バリデーション: zod(Edge対応)

デバッグとモニタリング

ログの確認

export const runtime = 'edge';

export async function GET(request: Request) {
  console.log('Edge function called from:', request.headers.get('x-vercel-ip-country'));

  return Response.json({ success: true });
}

Vercelダッシュボードの「Logs」タブでログを確認できます。

エラーハンドリング

export const runtime = 'edge';

export async function GET() {
  try {
    const data = await riskyOperation();
    return Response.json(data);
  } catch (error) {
    console.error('Error in edge function:', error);
    return Response.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

まとめ

Vercel Edge Functionsは、グローバルなアプリケーションに不可欠な技術です。

主要な利点:

  • 世界中のユーザーに低レイテンシでレスポンス
  • コールドスタートがほぼゼロ
  • 地理情報の簡単な取得
  • Next.jsとのシームレスな統合
  • 従量課金で低コスト

公式ドキュメント: https://vercel.com/docs/functions/edge-functions

グローバル展開を考えているなら、Edge Functionsは必須の選択肢です。今すぐNext.jsアプリに統合して、世界中のユーザーに高速な体験を提供しましょう。