Article

Vercelの中の人だった私が語る、Edge Functionsの本当の使いどころ

高田晃太郎
Vercelの中の人だった私が語る、Edge Functionsの本当の使いどころ

東京から米国東海岸までの往復遅延はおおむね150〜200ms前後という目安が一般に語られます(環境により大きく変動します)¹。研究・実務の両面で、ネットワーク往復(RTT)の削減はTTFB(Time to First Byte:最初のバイトが届くまでの時間)の短縮に直結し²、ユーザー行動にも影響を与えうることが示されています³。たとえばDeloitteの分析では、ページ応答が0.1秒改善するとコンバージョンに寄与するケースが報告されています³。公開事例や一般的な観測でも、エッジ実行環境で静的に近い軽量ロジックを配置した場合、グローバル配信のp95(95パーセンタイル)TTFBが改善するケースが確認されます²⁴。

一方で、「エッジなら常に速い」というのは誤解です。実行環境の制約、オリジン到達のチャッタネス(多数の小さな往復)、そしてキャッシュ戦略の粗さが足を引っ張ります。ここでは、私が現場の実装と公開情報の双方を踏まえ、Edge Functionsを本当に使うべき仕事と、そうでない設計を、コードと計測の観点から具体的に示します。

Edge Functionsの正体と誤解をほどく

VercelのEdge FunctionsはV8 Isolates上で動作し、Node.jsのコアAPIには依存せず、標準的なWeb API(Fetch、Request/Response、WebCryptoのSubtle Cryptoなど)を備えています。コールドスタートはミリ秒単位を目指した設計で、短寿命・多並列に最適化されています⁴。これが距離起因のRTTを吸収する形でTTFB短縮に効く場面があります²。対照的に、一般的なサーバレス関数(Nodeランタイム)はリージョン固定になりがちで、ユーザーとリージョン間の距離コストが顕在化しやすいという違いがあります¹。

なぜ速いのかの要点は、実行場所と応答の早出しの二点です。まずPOP(Point of Presence)がユーザーの近くにあることで、TCP/TLSハンドシェイクとデータ搬送の距離コストが下がります¹²。さらに、ストリーミング応答や早期フラッシュ(early flush)でレンダリングを先行させれば、LCP(Largest Contentful Paint)やINP(Interaction to Next Paint)といったCore Web Vitalsの改善に寄与します⁵。

ではどこで詰まるのか。重いNPM依存、ヘッドレスブラウザ、TCPソケット必須のDBドライバ、そしてチャッタネスの高いバックエンド到達が代表例です。エッジからオリジンへ何度も小さなリクエストを投げると、距離短縮で稼いだ貯金が一瞬で消えます。依存は小さくまとめ、外部到達は少数で大きくが原則です⁶⁷。

制約ベースで考える実装原則

Web標準APIで書ける処理は相性が良い一方、Node専用APIは避けるべきです。鍵素材はWebCryptoで扱い、署名検証や暗号化はEdge互換の軽量ライブラリ(例: jose)で行う。データはKVやHTTPベースのAPI経由で取り出し、TCPドライバ直結を避ける。これらを守ると、バンドルが小さくなり、起動も速く、キャッシュも効かせやすくなります⁶。

本当の使いどころをコードで掴む

地域・デバイス・言語のコンテキストでの分岐は、エッジの即断に最適です。たとえば国別価格の出し分け、法務的なジオフェンス、言語の自動ネゴシエーションなどは、オリジン到達前に確定できます⁶。

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

export const config = { matcher: ['/'] }

export default function middleware(req: NextRequest) {
  const country = req.geo?.country || 'US'
  const url = req.nextUrl.clone()
  if (country === 'JP') url.searchParams.set('currency', 'JPY')
  url.searchParams.set('ab', pickVariant(req))
  const res = NextResponse.rewrite(url)
  res.headers.set('Vary', 'Accept-Language, Cookie, Geo-Country')
  return res
}

function pickVariant(req: NextRequest) {
  const cookie = req.cookies.get('ab')?.value
  if (cookie) return cookie
  const v = Math.random() < 0.5 ? 'A' : 'B'
  return v
}

認証トークンの軽量検証も王道です。公開鍵での署名検証はWebCryptoで高速にこなせます。秘密鍵を持たない検証パスはエッジに、リフレッシュや発行はオリジンに寄せるのが分業の基本です。

// app/api/me/route.ts (edge)
export const runtime = 'edge'
import { jwtVerify, createRemoteJWKSet } from 'jose'

const jwks = createRemoteJWKSet(new URL('https://idp.example.com/.well-known/jwks.json'))

export async function GET(req: Request) {
  try {
    const auth = req.headers.get('authorization') || ''
    const token = auth.replace('Bearer ', '')
    const { payload } = await jwtVerify(token, jwks, { issuer: 'https://idp.example.com' })
    return new Response(JSON.stringify({ sub: payload.sub, roles: payload.roles }), {
      headers: { 'Content-Type': 'application/json', 'Cache-Control': 'private, max-age=60' },
    })
  } catch (e) {
    return new Response(JSON.stringify({ error: 'unauthorized' }), { status: 401 })
  }
}

A/Bテストやフラグの配信は、キャッシュと相性が良いときに威力を発揮します。変数を決めるのはエッジ、重いコンポーネントのレンダリングは後段に任せ、CDNキャッシュキーにバリアントを織り込みます。Vercel Edge ConfigのようなHTTP到達の設定ストアを使うと、低遅延で一貫性の高いフラグ配信ができます⁸。

// app/api/flag/route.ts (edge)
export const runtime = 'edge'
import { get } from '@vercel/edge-config'

export async function GET() {
  const rollout = await get<number>('search-new-ui-rollout')
  const variant = Math.random() * 100 < (rollout ?? 0) ? 'new' : 'old'
  return new Response(JSON.stringify({ variant }), {
    headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=30, stale-while-revalidate=600' },
  })
}

言語ネゴシエーションとプリロードヒントは、最初のバイトを早出ししつつリソースの先読みを段取ると効果が高まります。Varyを適切に設定し、キャッシュの粒度を制御します²。

// app/api/i18n/route.ts (edge)
export const runtime = 'edge'

function detectLang(req: Request) {
  const al = req.headers.get('accept-language') || ''
  return al.startsWith('ja') ? 'ja' : 'en'
}

export async function GET(req: Request) {
  const lang = detectLang(req)
  const body = lang === 'ja' ? 'こんにちは' : 'Hello'
  return new Response(body, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'Cache-Control': 'public, max-age=300',
      'Vary': 'Accept-Language',
      'Link': '</static/'+lang+'.css>; rel=preload; as=style'
    }
  })
}

ストリーミングと早期フラッシュは、エッジの近さと相乗効果があります。重い集約は避けつつ、先頭HTMLを先に返し、後から差し込む構成にします⁵。

// app/api/stream/route.ts (edge)
export const runtime = 'edge'

export async function GET() {
  const { readable, writable } = new TransformStream()
  const writer = writable.getWriter()
  queueMicrotask(async () => {
    await writer.write(new TextEncoder().encode('<h1>First byte</h1>'))
    const data = await fetch('https://api.example.com/lightweight').then(r => r.text())
    await writer.write(new TextEncoder().encode('<div>'+data+'</div>'))
    await writer.close()
  })
  return new Response(readable, { headers: { 'Content-Type': 'text/html', 'Cache-Control': 'no-store' } })
}

キャッシュ制御とエラーの設計

エッジはキャッシュと一体で考えると成功率が上がります。パーソナライズがある場合でも、Varyヘッダーと短いmax-age、そしてstale-while-revalidateで堅牢性を高めます。到達不能時に壊れないためのフォールバックもエッジで実装しておくと、サービス品質(SLA/SLO)を守りやすくなります²。

// app/api/content/route.ts (edge)
export const runtime = 'edge'

export async function GET() {
  try {
    const r = await fetch('https://origin.example.com/content', { cache: 'no-store' })
    if (!r.ok) throw new Error('upstream')
    const res = new Response(r.body, r)
    res.headers.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=600')
    return res
  } catch {
    return new Response('<p>stale content</p>', { status: 200, headers: { 'Cache-Control': 'public, max-age=30' } })
  }
}

使わないほうがよいケースと回避策

ヘッドレスブラウザでのスクレイピングやPDF生成、画像の重い変換、大規模な機械学習推論、TCPソケット直結のデータベース接続などはエッジに不向きです。これらはリージョン固定のサーバレス関数や専用のワーカー、キューを使った非同期実行に逃がすのが健全です⁶⁷。エッジは判定と軽量整形に特化し、重い処理はジョブキューへ委譲することで、待ち時間を表面化させずに済みます。

HTTPベースのデータアクセスは有効な選択肢です。GraphQLゲートウェイやRESTの集約エンドポイントを一段挟み、1往復で必要なフィールドを取るようにモデル化します。併せてスキーマに時間制約を埋め込み、SLOに合わないクエリは落とすかデフォルトを返す方針にしておくと、エッジの短時間実行と矛盾しません⁶。

依存サイズはパフォーマンスの敵です。暗号やJWTなどはEdge互換の軽量実装を選び、国際化や日付整形は必要なロケールだけを取り込む。これだけで数百KB規模の削減につながり、起動遅延とコストの両面で効いてきます²。

ビジネスインパクトをどう測り、どう示すか

距離の短縮は数値で語るべきです。公開された分析では、TTFBやLCPの改善が直帰率やコンバージョンの改善と相関するケースが示されています²³⁵。実務では、リージョン固定のサーバレスから「エッジでの判定+キャッシュ前段」に切り替えることで、p95 TTFBが有意に短縮されることがあります²⁴。もちろんコンテンツや導線の影響もあるため単独効果の分離は容易ではありませんが、TTFBとLCPの改善がビジネス指標に波及しやすいことは多くの現場で再現性があります。

導入のROIは、簡易なパーソナライズやA/Bの出し分け、トークン検証のオフロードから立ち上げるのが現実的です。既存のバックエンドに手を入れず、ミドルウェアと数本のEdge APIルートを追加するだけで効果を確認できます。うまくいけばスコープを広げ、エッジとサーバレス、さらには長時間実行のバッチやキューの役割分担を明確化していく。費用面も、バンドルを小さく保ち、キャッシュヒット率を高めれば、リクエスト単価の増分を相殺しやすくなります⁶。

最後に、計測の再現性を担保するためのベンチマーク例を示します。ローカルではなく、実際のユーザーに近いロケーションからの計測が重要です¹。

# k6でエッジとリージョン固定の差を観測(ロケーションはCDN側に近いものを選ぶ)
k6 run --vus 50 --duration 60s edge.js

# autocannonでTTFBとp95を比較(近距離/遠距離の両方で)
autocannon -c 100 -d 30 -p 10 https://edge.example.com/api/stream
autocannon -c 100 -d 30 -p 10 https://region-us-east.example.com/api/stream

ここまで触れた原則は、距離の短縮、チャッタネスの削減、軽量依存、キャッシュ前提の設計、そしてエラー時の優雅な劣化という五点に集約されます。これらを守るだけで、Edge Functionsは万能ツールから特定の課題に極めて強い道具へと姿を変え、費用対効果の説明がしやすくなります。

まとめ:距離を設計に織り込むという発想

Edge Functionsは、アプリケーションの一部をユーザーの近くで即断・即返しするための実行環境です。万能ではないものの、判定・軽量整形・キャッシュの三拍子が揃う領域では、距離の壁を超えて体感を大きく変えられます。まずはミドルウェアでの出し分け、JWT検証、軽いフラグ配信の三点から小さく始め、TTFBとLCPの実測を同時に取りに行ってください⁵。改善が確認できた箇所にスコープを広げ、重い処理はサーバレスやキューへ段階的に逃がすという、役割分担の設計に舵を切るのがよいでしょう。

次に着手するのは、キャッシュキーとVaryの整理、依存の軽量化、そして障害時のフォールバックです。どの地点でどの程度の遅延を削れるのか、ダッシュボードで可視化しながら、距離を味方に付ける設計へ。あなたのプロダクトのどのエンドポイントなら、明日からエッジに移せますか。

参考文献

  1. AWS. How to identify website performance bottlenecks by measuring Time to First Byte (TTFB) latency and using Server-Timing header. https://aws.amazon.com/blogs/networking-and-content-delivery/how-to-identify-website-performance-bottlenecks-by-measuring-time-to-first-byte-latency-and-using-server-timing-header/

  2. web.dev. Optimize TTFB. https://web.dev/articles/optimize-ttfb

  3. Deloitte. Milliseconds make millions. https://www2.deloitte.com/ie/en/services/consulting/research/milliseconds-make-millions.html

  4. Vercel Docs. Edge Functions. https://vercel.com/docs/functions/runtimes/edge/edge-functions

  5. web.dev. Optimize LCP. https://web.dev/optimize-lcp/

  6. AWS. Lambda@Edge design best practices. https://aws.amazon.com/blogs/networking-and-content-delivery/lambdaedge-design-best-practices/

  7. Cloudflare Docs. Workers platform limits. https://developers.cloudflare.com/workers/platform/limits/

  8. Vercel Docs. Edge Config. https://vercel.com/storage/edge-config