コンテンツへスキップ
THE GUILD
0%
サービス プロダクト 採用情報 会社概要 ブログ よくある質問 お問い合わせ
フロントエンド開発者のためのエッジコンピューティング:完全ガイド2026

エッジコンピューティングはインフラの関心事から重要なフロントエンド開発スキルへと変化しました。2026年、フロントエンドとバックエンドの境界線は、開発者が世界中のユーザーからミリ秒以内の場所で実行されるコードをデプロイするにつれて、ますます曖昧になっています。この包括的なガイドでは、フロントエンド開発者がエッジコンピューティングについて知っておくべきすべて—基本概念から本番対応の実装まで—をカバーします。

既存のアプリケーションを最適化する場合でも、新しいシステムを設計する場合でも、エッジコンピューティングの理解は、現代の高性能Webアプリケーションを構築するために不可欠です。

フロントエンド開発者にとってのエッジコンピューティングとは?

エッジコンピューティングは、計算とデータストレージをデータソースに近づけます。フロントエンド開発者にとって、これは中央集中型データセンターではなく、ユーザーの近くにある分散サーバーでサーバーサイドロジックを実行することを意味します。

従来のアーキテクチャ vs エッジアーキテクチャ

従来のアーキテクチャ:

東京のユーザー → CDN(静的ファイル) → バージニアのオリジンサーバー → データベース

                              500ms以上のレイテンシ

エッジアーキテクチャ:

東京のユーザー → 東京のエッジサーバー → オリジン(必要に応じて)

               50msのレイテンシ
               (動的コンテンツがエッジでレンダリング)

フロントエンド開発者にエッジスキルが必要な理由

Webプラットフォームの進化により、「フロントエンド」の意味が根本的に変わりました:

  1. サーバーコンポーネント:React Server Components、Astroなどの技術がクライアント-サーバーの境界を曖昧にする
  2. エッジレンダリング:パーソナライズされた動的コンテンツのためのエッジでのSSRとSSG
  3. APIルート:フロントエンドフレームワークにバックエンド機能が含まれるように
  4. リアルタイム機能:WebSocketsとストリーミングのためのエッジネイティブソリューション

エッジプラットフォーム:フロントエンド開発者向けガイド

複数のプラットフォームがエッジコンピューティング機能を提供しています。それぞれの強みを理解することで、適切なツールを選択できます。

Vercel Edge Functions

Vercel Edge FunctionsはV8アイソレートを活用したVercelのエッジネットワーク上で実行されます。Next.jsアプリケーションに最適です。

主な機能:

  • サブミリ秒のコールドスタート
  • ストリーミングレスポンス
  • デフォルトでグローバルデプロイ
  • ネイティブNext.js統合

基本的なエッジ関数:

// app/api/hello/route.ts
export const runtime = 'edge';

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

  return new Response(`Hello, ${name}!`, {
    headers: {
      'content-type': 'text/plain',
    },
  });
}

エッジミドルウェア:

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

export function middleware(request: NextRequest) {
  // エッジでユーザーの国を取得
  const country = request.geo?.country || 'US';

  // 場所に基づいてパーソナライズ
  const response = NextResponse.next();
  response.headers.set('x-user-country', country);

  // エッジでのA/Bテスト
  const bucket = Math.random() < 0.5 ? 'control' : 'experiment';
  response.cookies.set('ab-bucket', bucket);

  return response;
}

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

Cloudflare Workers

Cloudflare Workersは最も成熟したエッジコンピューティングプラットフォームを提供し、広範なAPIを備えています。

主な機能:

  • 世界200以上のエッジロケーション
  • エッジデータ用KVストレージ
  • ステートフルアプリケーション用Durable Objects
  • エッジ機械学習用Workers AI

基本的なWorker:

// src/index.js
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    if (url.pathname === '/api/data') {
      // エッジでKVストアからフェッチ
      const cachedData = await env.MY_KV.get('data-key', 'json');

      if (cachedData) {
        return Response.json(cachedData);
      }

      // オリジンからフェッチしてキャッシュ
      const response = await fetch('https://api.origin.com/data');
      const data = await response.json();

      // 1時間キャッシュ
      ctx.waitUntil(env.MY_KV.put('data-key', JSON.stringify(data), {
        expirationTtl: 3600
      }));

      return Response.json(data);
    }

    return new Response('Not Found', { status: 404 });
  }
};

Wrangler設定:

# wrangler.toml
name = "my-edge-app"
main = "src/index.js"
compatibility_date = "2026-01-01"

[[kv_namespaces]]
binding = "MY_KV"
id = "abc123"

[vars]
ENVIRONMENT = "production"

Deno Deploy

Deno DeployはファーストクラスのTypeScriptサポートとWeb標準APIを備えたエッジコンピューティングを提供します。

主な機能:

  • 箱から出してTypeScript
  • Web標準API
  • 組み込みKVデータベース
  • GitHub統合

Denoエッジ関数:

// main.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";

const kv = await Deno.openKv();

serve(async (request: Request) => {
  const url = new URL(request.url);

  if (url.pathname === "/api/counter") {
    // エッジでのアトミックインクリメント
    const key = ["counters", "visits"];
    const result = await kv.atomic()
      .sum(key, 1n)
      .commit();

    const entry = await kv.get(key);

    return Response.json({
      count: entry.value?.toString() || "0"
    });
  }

  return new Response("Hello from the Edge!");
}, { port: 8000 });

AWS CloudFront Functions & Lambda@Edge

AWSは異なる機能を持つ2つのエッジコンピューティングオプションを提供しています。

CloudFront Functions(軽量):

function handler(event) {
  var request = event.request;
  var headers = request.headers;

  // エッジでセキュリティヘッダーを追加
  var response = {
    statusCode: 200,
    statusDescription: 'OK',
    headers: {
      'strict-transport-security': { value: 'max-age=31536000' },
      'content-security-policy': { value: "default-src 'self'" },
      'x-content-type-options': { value: 'nosniff' },
      'x-frame-options': { value: 'DENY' }
    }
  };

  return response;
}

Lambda@Edge(フルコンピュート):

exports.handler = async (event) => {
  const request = event.Records[0].cf.request;

  // エッジでのパーソナライゼーション
  const userAgent = request.headers['user-agent'][0].value;
  const isMobile = /Mobile|Android/.test(userAgent);

  if (isMobile) {
    request.uri = request.uri.replace(/\.html$/, '.mobile.html');
  }

  return request;
};

一般的なエッジコンピューティングパターン

フロントエンド開発者が頻繁に実装するエッジパターン。

パターン1:エッジでのパーソナライゼーション

オリジンへのラウンドトリップなしでパーソナライズされたコンテンツを配信:

// パーソナライゼーション用Vercelエッジミドルウェア
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // パーソナライゼーションコンテキストを取得
  const country = request.geo?.country || 'US';
  const city = request.geo?.city || 'Unknown';
  const userAgent = request.headers.get('user-agent') || '';

  // コンテンツバリアントを決定
  const isMobile = /Mobile|Android|iPhone/.test(userAgent);
  const locale = getPreferredLocale(request.headers.get('accept-language'));
  const currency = getCurrencyForCountry(country);

  // ダウンストリーム使用のためにヘッダーを設定
  response.headers.set('x-personalization', JSON.stringify({
    country,
    city,
    isMobile,
    locale,
    currency
  }));

  // パーソナライズされたコンテンツにリライト
  if (request.nextUrl.pathname === '/') {
    return NextResponse.rewrite(
      new URL(`/${locale}/home?currency=${currency}`, request.url),
      { headers: response.headers }
    );
  }

  return response;
}

function getPreferredLocale(acceptLanguage: string | null): string {
  if (!acceptLanguage) return 'en';

  const locales = ['en', 'ja', 'de', 'fr', 'es', 'zh'];
  const preferred = acceptLanguage.split(',')[0].split('-')[0];

  return locales.includes(preferred) ? preferred : 'en';
}

function getCurrencyForCountry(country: string): string {
  const currencyMap: Record<string, string> = {
    US: 'USD',
    JP: 'JPY',
    GB: 'GBP',
    DE: 'EUR',
    MY: 'MYR'
  };

  return currencyMap[country] || 'USD';
}

パターン2:エッジでのA/Bテスト

クライアントサイドのフリッカーなしで一貫したA/Bテストを実装:

// A/Bテスト用Cloudflare Worker
interface Env {
  EXPERIMENTS_KV: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // 実験バケットを取得または割り当て
    const cookies = parseCookies(request.headers.get('cookie'));
    let bucket = cookies['ab-experiment'];

    if (!bucket) {
      // 新しいユーザーを実験に割り当て
      const experiment = await env.EXPERIMENTS_KV.get('homepage-v2', 'json') as {
        variants: { id: string; weight: number }[];
      };

      bucket = assignVariant(experiment.variants);
    }

    // 実験バリアントにリライト
    if (url.pathname === '/') {
      const variantUrl = new URL(url);
      variantUrl.pathname = `/experiments/${bucket}/index.html`;

      const response = await fetch(variantUrl);
      const modifiedResponse = new Response(response.body, response);

      // 一貫したエクスペリエンスのためにCookieを設定
      modifiedResponse.headers.append(
        'Set-Cookie',
        `ab-experiment=${bucket}; Path=/; Max-Age=2592000; SameSite=Lax`
      );

      // 分析のために割り当てを追跡
      await trackExperimentAssignment(bucket, request);

      return modifiedResponse;
    }

    return fetch(request);
  }
};

function assignVariant(variants: { id: string; weight: number }[]): string {
  const random = Math.random();
  let cumulative = 0;

  for (const variant of variants) {
    cumulative += variant.weight;
    if (random < cumulative) {
      return variant.id;
    }
  }

  return variants[0].id;
}

パターン3:Edge-Side Includes (ESI)

エッジでキャッシュされたフラグメントからページを構成:

// ESIスタイルコンポジション用エッジ関数
export async function onRequest(context: EventContext) {
  const url = new URL(context.request.url);

  if (url.pathname.startsWith('/product/')) {
    // ページコンポーネントを並列でフェッチ
    const [header, productContent, recommendations, footer] = await Promise.all([
      fetchCachedFragment('/fragments/header'),
      fetchProductContent(url.pathname),
      fetchPersonalizedRecommendations(context.request),
      fetchCachedFragment('/fragments/footer')
    ]);

    // 最終HTMLを構成
    const html = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>${productContent.title}</title>
        </head>
        <body>
          ${header}
          <main>${productContent.html}</main>
          <aside>${recommendations}</aside>
          ${footer}
        </body>
      </html>
    `;

    return new Response(html, {
      headers: {
        'content-type': 'text/html',
        'cache-control': 'public, max-age=60, stale-while-revalidate=300'
      }
    });
  }

  return context.next();
}

async function fetchCachedFragment(path: string): Promise<string> {
  // フラグメントはエッジで1時間キャッシュ
  const response = await fetch(`https://origin.com${path}`, {
    cf: { cacheTtl: 3600 }
  });
  return response.text();
}

async function fetchPersonalizedRecommendations(request: Request): Promise<string> {
  // Cookie/geoに基づいたパーソナライズされたコンテンツ
  const userId = getCookie(request, 'user_id');
  const country = request.cf?.country || 'US';

  const response = await fetch(
    `https://api.origin.com/recommendations?user=${userId}&country=${country}`
  );
  const data = await response.json();

  return renderRecommendations(data);
}

パターン4:エッジ認証

リクエストがオリジンに到達する前にエッジで認証を検証:

// エッジでのJWT検証
import { jwtVerify } from 'jose';

interface Env {
  JWT_SECRET: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // パブリックルートは認証をスキップ
    if (isPublicRoute(url.pathname)) {
      return fetch(request);
    }

    // JWTを抽出して検証
    const authHeader = request.headers.get('authorization');

    if (!authHeader?.startsWith('Bearer ')) {
      return new Response('Unauthorized', { status: 401 });
    }

    const token = authHeader.slice(7);

    try {
      const secret = new TextEncoder().encode(env.JWT_SECRET);
      const { payload } = await jwtVerify(token, secret);

      // リクエストにユーザーコンテキストを追加
      const modifiedHeaders = new Headers(request.headers);
      modifiedHeaders.set('x-user-id', payload.sub as string);
      modifiedHeaders.set('x-user-role', payload.role as string);

      const modifiedRequest = new Request(request, {
        headers: modifiedHeaders
      });

      return fetch(modifiedRequest);
    } catch (error) {
      return new Response('Invalid token', { status: 401 });
    }
  }
};

function isPublicRoute(pathname: string): boolean {
  const publicRoutes = ['/login', '/register', '/api/health', '/public'];
  return publicRoutes.some(route => pathname.startsWith(route));
}

パターン5:Stale-While-Revalidateを使用したスマートキャッシング

エッジでインテリジェントなキャッシング戦略を実装:

// スマートキャッシング付きCloudflare Worker
interface Env {
  CACHE_KV: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const cacheKey = new URL(request.url).pathname;

    // エッジキャッシュから取得を試行
    const cached = await env.CACHE_KV.getWithMetadata(cacheKey, 'json');

    if (cached.value) {
      const metadata = cached.metadata as { expires: number; stale: number };
      const now = Date.now();

      if (now < metadata.expires) {
        // フレッシュキャッシュヒット
        return Response.json(cached.value, {
          headers: { 'x-cache': 'HIT' }
        });
      }

      if (now < metadata.stale) {
        // Stale-while-revalidate
        ctx.waitUntil(revalidateCache(request, env, cacheKey));

        return Response.json(cached.value, {
          headers: { 'x-cache': 'STALE' }
        });
      }
    }

    // キャッシュミス - オリジンからフェッチ
    return revalidateCache(request, env, cacheKey);
  }
};

async function revalidateCache(
  request: Request,
  env: Env,
  cacheKey: string
): Promise<Response> {
  const response = await fetch(`https://api.origin.com${cacheKey}`);
  const data = await response.json();

  // TTLメタデータ付きで保存
  const now = Date.now();
  await env.CACHE_KV.put(cacheKey, JSON.stringify(data), {
    metadata: {
      expires: now + 60000,      // 1分間フレッシュ
      stale: now + 300000        // 5分間ステール
    }
  });

  return Response.json(data, {
    headers: { 'x-cache': 'MISS' }
  });
}

エッジデータストレージ

エッジコンピューティングにはエッジネイティブのデータストレージソリューションが必要です。

Cloudflare KV

読み取り重視のワークロードに最適化されたキーバリューストレージ:

// エッジでのKV操作
interface Env {
  USER_PREFERENCES: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const userId = url.searchParams.get('userId');

    if (request.method === 'GET') {
      // ユーザー設定を読み取り
      const prefs = await env.USER_PREFERENCES.get(`user:${userId}`, 'json');
      return Response.json(prefs || { theme: 'light', language: 'en' });
    }

    if (request.method === 'POST') {
      // 設定を更新
      const body = await request.json();

      await env.USER_PREFERENCES.put(`user:${userId}`, JSON.stringify(body), {
        expirationTtl: 86400 * 30  // 30日
      });

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

    return new Response('Method not allowed', { status: 405 });
  }
};

Cloudflare Durable Objects

リアルタイムアプリケーション用のエッジでのステートフルコンピュート:

// リアルタイムコラボレーション用Durable Object
export class DocumentRoom {
  state: DurableObjectState;
  sessions: Map<WebSocket, { userId: string }>;
  document: string;

  constructor(state: DurableObjectState) {
    this.state = state;
    this.sessions = new Map();
    this.document = '';
  }

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === '/websocket') {
      // WebSocketアップグレードを処理
      const pair = new WebSocketPair();
      const [client, server] = Object.values(pair);

      const userId = url.searchParams.get('userId') || 'anonymous';
      this.handleSession(server, userId);

      return new Response(null, { status: 101, webSocket: client });
    }

    return new Response('Not found', { status: 404 });
  }

  handleSession(webSocket: WebSocket, userId: string) {
    webSocket.accept();
    this.sessions.set(webSocket, { userId });

    // 現在のドキュメント状態を送信
    webSocket.send(JSON.stringify({
      type: 'sync',
      document: this.document,
      users: Array.from(this.sessions.values()).map(s => s.userId)
    }));

    webSocket.addEventListener('message', (event) => {
      const message = JSON.parse(event.data as string);

      if (message.type === 'edit') {
        // 編集を適用してブロードキャスト
        this.document = applyEdit(this.document, message.edit);

        this.broadcast({
          type: 'edit',
          userId,
          edit: message.edit
        }, webSocket);
      }
    });

    webSocket.addEventListener('close', () => {
      this.sessions.delete(webSocket);
      this.broadcast({ type: 'user_left', userId });
    });
  }

  broadcast(message: object, exclude?: WebSocket) {
    const json = JSON.stringify(message);

    for (const [ws] of this.sessions) {
      if (ws !== exclude) {
        ws.send(json);
      }
    }
  }
}

Deno KV

Deno Deploy用の組み込みキーバリューデータベース:

// エッジデータ用Deno KV
const kv = await Deno.openKv();

// アトミックトランザクション
async function transferBalance(fromId: string, toId: string, amount: number) {
  const fromKey = ["balances", fromId];
  const toKey = ["balances", toId];

  // アトミックチェック&アップデート
  let success = false;

  while (!success) {
    const fromEntry = await kv.get<number>(fromKey);
    const toEntry = await kv.get<number>(toKey);

    const fromBalance = fromEntry.value || 0;
    const toBalance = toEntry.value || 0;

    if (fromBalance < amount) {
      throw new Error("残高不足");
    }

    const result = await kv.atomic()
      .check(fromEntry)
      .check(toEntry)
      .set(fromKey, fromBalance - amount)
      .set(toKey, toBalance + amount)
      .commit();

    success = result.ok;
  }

  return { success: true };
}

// セカンダリインデックス
async function addUser(user: { id: string; email: string; name: string }) {
  await kv.atomic()
    .set(["users", user.id], user)
    .set(["users_by_email", user.email], user.id)
    .commit();
}

async function getUserByEmail(email: string) {
  const idEntry = await kv.get<string>(["users_by_email", email]);
  if (!idEntry.value) return null;

  const userEntry = await kv.get(["users", idEntry.value]);
  return userEntry.value;
}

エッジでのパフォーマンス最適化

これらの最適化技術でエッジコンピューティングの利点を最大化。

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

生成されるコンテンツをストリーム:

// エッジからのストリーミングレスポンス
export async function onRequest(context: EventContext): Promise<Response> {
  const { readable, writable } = new TransformStream();
  const writer = writable.getWriter();
  const encoder = new TextEncoder();

  // 即座にストリーミング開始
  context.waitUntil((async () => {
    // HTMLヘッドを即座に書き込み
    await writer.write(encoder.encode(`
      <!DOCTYPE html>
      <html>
        <head><title>Streaming Page</title></head>
        <body>
          <header>コンテンツを読み込み中...</header>
    `));

    // メインコンテンツが利用可能になったらストリーム
    const mainContent = await fetchMainContent();
    await writer.write(encoder.encode(`
          <main>${mainContent}</main>
    `));

    // サイドバーをストリーム
    const sidebarContent = await fetchSidebarContent();
    await writer.write(encoder.encode(`
          <aside>${sidebarContent}</aside>
    `));

    // ページを完成
    await writer.write(encoder.encode(`
        </body>
      </html>
    `));

    await writer.close();
  })());

  return new Response(readable, {
    headers: { 'content-type': 'text/html' }
  });
}

リクエストコアレッシング

キャッシュミス時のサンダリングハードを防止:

// エッジでのリクエストコアレッシング
const inflightRequests = new Map<string, Promise<Response>>();

export async function onRequest(context: EventContext): Promise<Response> {
  const cacheKey = context.request.url;

  // リクエストがすでに進行中かチェック
  const inflight = inflightRequests.get(cacheKey);
  if (inflight) {
    // 既存のリクエストを待機
    const response = await inflight;
    return response.clone();
  }

  // 新しいリクエストを作成してプロミスを保存
  const fetchPromise = fetchWithCache(context.request);
  inflightRequests.set(cacheKey, fetchPromise);

  try {
    const response = await fetchPromise;
    return response;
  } finally {
    // 完了後にクリーンアップ
    inflightRequests.delete(cacheKey);
  }
}

async function fetchWithCache(request: Request): Promise<Response> {
  const cache = caches.default;

  let response = await cache.match(request);

  if (!response) {
    response = await fetch(request);

    // 成功したレスポンスをキャッシュ
    if (response.ok) {
      const cloned = response.clone();
      cloned.headers.set('Cache-Control', 'public, max-age=60');
      await cache.put(request, cloned);
    }
  }

  return response;
}

エッジ最適化画像

エッジで画像を変換・最適化:

// エッジでの画像最適化
export async function onRequest(context: EventContext): Promise<Response> {
  const url = new URL(context.request.url);

  if (!url.pathname.startsWith('/images/')) {
    return context.next();
  }

  // 最適化パラメータを解析
  const width = parseInt(url.searchParams.get('w') || '0');
  const quality = parseInt(url.searchParams.get('q') || '80');
  const format = url.searchParams.get('f') || 'auto';

  // Acceptヘッダーに基づいて最適なフォーマットを決定
  const accept = context.request.headers.get('accept') || '';
  const outputFormat = format === 'auto'
    ? accept.includes('image/avif') ? 'avif'
    : accept.includes('image/webp') ? 'webp'
    : 'jpeg'
    : format;

  // Cloudflare Image Resizingを使用
  const imageUrl = `https://origin.com${url.pathname}`;

  const response = await fetch(imageUrl, {
    cf: {
      image: {
        width: width || undefined,
        quality,
        format: outputFormat
      }
    }
  });

  // キャッシュヘッダーを追加
  const headers = new Headers(response.headers);
  headers.set('Cache-Control', 'public, max-age=31536000, immutable');
  headers.set('Vary', 'Accept');

  return new Response(response.body, {
    status: response.status,
    headers
  });
}

エッジ関数のテスト

エッジコードのための包括的なテスト戦略。

ローカル開発

エッジ関数をローカルで実行:

// ローカルCloudflare Workersテスト用Miniflare
import { Miniflare } from 'miniflare';

const mf = new Miniflare({
  script: `
    export default {
      async fetch(request, env) {
        return new Response('Hello from edge!');
      }
    }
  `,
  modules: true,
  kvNamespaces: ['MY_KV'],
});

const response = await mf.dispatchFetch('http://localhost/');
console.log(await response.text()); // "Hello from edge!"

ユニットテスト

エッジ関数を分離してテスト:

// エッジ関数用Jestテスト
import { unstable_dev } from 'wrangler';

describe('Edge API', () => {
  let worker: any;

  beforeAll(async () => {
    worker = await unstable_dev('src/index.ts', {
      experimental: { disableExperimentalWarning: true }
    });
  });

  afterAll(async () => {
    await worker.stop();
  });

  test('パーソナライズされた挨拶を返す', async () => {
    const response = await worker.fetch('/api/greet?name=World');
    const text = await response.text();

    expect(response.status).toBe(200);
    expect(text).toBe('Hello, World!');
  });

  test('パラメータがない場合を処理', async () => {
    const response = await worker.fetch('/api/greet');
    const text = await response.text();

    expect(text).toBe('Hello, Guest!');
  });
});

インテグレーションテスト

実際の依存関係を持つエッジ関数をテスト:

// KVストレージとのインテグレーションテスト
describe('Edge Cache', () => {
  let mf: Miniflare;
  let kv: any;

  beforeEach(async () => {
    mf = new Miniflare({
      scriptPath: './src/index.ts',
      modules: true,
      kvNamespaces: ['CACHE']
    });

    kv = await mf.getKVNamespace('CACHE');
  });

  test('APIレスポンスをキャッシュ', async () => {
    // 最初のリクエスト - キャッシュミス
    const response1 = await mf.dispatchFetch('http://localhost/api/data');
    expect(response1.headers.get('x-cache')).toBe('MISS');

    // 2回目のリクエスト - キャッシュヒット
    const response2 = await mf.dispatchFetch('http://localhost/api/data');
    expect(response2.headers.get('x-cache')).toBe('HIT');

    // KVにデータがあることを確認
    const cached = await kv.get('/api/data', 'json');
    expect(cached).toBeDefined();
  });
});

セキュリティ考慮事項

エッジコンピューティングは独自のセキュリティ課題をもたらします。

エッジでの入力検証

すべての入力を検証・サニタイズ:

// 入力検証ミドルウェア
export async function middleware(request: NextRequest) {
  const url = new URL(request.url);

  // クエリパラメータを検証
  for (const [key, value] of url.searchParams) {
    if (!isValidParameter(key, value)) {
      return new Response('Bad Request', { status: 400 });
    }
  }

  // POST/PUTのリクエストボディを検証
  if (['POST', 'PUT', 'PATCH'].includes(request.method)) {
    try {
      const body = await request.json();

      if (!validateRequestBody(body)) {
        return new Response('Invalid request body', { status: 400 });
      }
    } catch {
      return new Response('Invalid JSON', { status: 400 });
    }
  }

  return NextResponse.next();
}

function isValidParameter(key: string, value: string): boolean {
  // 長さ制限
  if (key.length > 64 || value.length > 1024) return false;

  // 文字検証
  if (!/^[\w-]+$/.test(key)) return false;

  // SQLインジェクションパターン
  const sqlPatterns = /('|"|;|--|\b(SELECT|INSERT|UPDATE|DELETE|DROP)\b)/i;
  if (sqlPatterns.test(value)) return false;

  return true;
}

レート制限

エッジでレート制限を実装:

// Cloudflare KVを使用したエッジレート制限
interface Env {
  RATE_LIMITS: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const clientIP = request.headers.get('cf-connecting-ip') || 'unknown';
    const windowKey = `ratelimit:${clientIP}:${Math.floor(Date.now() / 60000)}`;

    // 現在のカウントを取得
    const current = await env.RATE_LIMITS.get(windowKey);
    const count = current ? parseInt(current) : 0;

    // 制限をチェック(1分あたり100リクエスト)
    if (count >= 100) {
      return new Response('Rate limit exceeded', {
        status: 429,
        headers: {
          'Retry-After': '60',
          'X-RateLimit-Limit': '100',
          'X-RateLimit-Remaining': '0'
        }
      });
    }

    // カウンターをインクリメント
    await env.RATE_LIMITS.put(windowKey, String(count + 1), {
      expirationTtl: 120  // 2分のTTL
    });

    // リクエストを処理
    const response = await fetch(request);
    const modifiedResponse = new Response(response.body, response);

    modifiedResponse.headers.set('X-RateLimit-Limit', '100');
    modifiedResponse.headers.set('X-RateLimit-Remaining', String(99 - count));

    return modifiedResponse;
  }
};

結論:エッジファースト開発を採用する

エッジコンピューティングはWeb開発の未来を表しています。アプリケーションがより低いレイテンシ、より高いパーソナライゼーション、グローバルリーチを要求するにつれて、エッジコンピューティングは卓越したユーザー体験を提供するアーキテクチャを提供します。

重要なポイント

  1. 適切なプラットフォームを選択:Next.jsにはVercel、複雑なワークロードにはCloudflare、TypeScriptファースト開発にはDeno
  2. エッジファーストで考える:最初からエッジ計算を念頭に置いてアプリケーションを設計
  3. エッジデータを活用:ステートフルエッジアプリケーションにはKVストアとDurable Objectsを使用
  4. ストリーミングに最適化:知覚パフォーマンスを向上させるためにレスポンスをストリーム
  5. 徹底的にテスト:ローカル開発とインテグレーションテストが不可欠

エッジコンピューティングの未来

2026年を通じて、エッジコンピューティングは進化し続けます:

  • エッジでのAI:エッジノードで実行される機械学習推論
  • WebAssemblyの拡張:より多くの言語がエッジプラットフォームで実行するようにコンパイル
  • より深い統合:フロントエンドフレームワークとエッジランタイムのより緊密な結合

エッジ開発の専門知識が必要ですか?

エッジネイティブアプリケーションを構築するには、分散システム、パフォーマンス最適化、現代のWebアーキテクチャに関する専門知識が必要です。当社のオフショア開発チームは、エッジコンピューティング技術を使用した高性能Webアプリケーションの構築を専門としています。

Webシステム開発を探索 SaaS開発について学ぶ


アプリケーションをエッジに移行する準備はできていますか?Webパフォーマンスの未来は分散型です。

どのようなエッジコンピューティングの課題に直面していますか?あなたの経験と質問を共有してください。


出典