Vercel Edge ランタイム: 高度なミドルウェア、地理位置情報、および A/B テスト
Vercel を使用すると、エッジ ミドルウェアが最初に SSR レンダリングを実行し、ルーティングが可能になります。 地理位置情報、機能フラグ、A/B テスト、認証に基づく コールドスタートなしでは、世界平均ですべてが 50 ミリ秒未満です。
レンダリング前のミドルウェア
Vercel にデプロイされた従来の Next.js アプリケーションでは、各リクエストは 最初に CDN に到達し、キャッシュにない場合は転送されます。 SSRサーバーに送信します。エッジ ミドルウェアは次の 2 つの段階の間に収まります。 SSR サーバーにルーティングする前に、Vercel ネットワークのエッジで実行されます。 応答がキャッシュから提供される前。
この位置付けは重要です。ミドルウェアは変更することができます。 リクエスト、ユーザーのリダイレクト、Cookie またはヘッダーなどの設定 V8 ベースのランタイムでコールド スタートがゼロに近い状態で発生します。ヴェルセル対策 ほとんどのミドルウェアのレイテンシは P50 < 10ms エッジ領域 (100 以上のグローバル)。
何を学ぶか
- Next.js App Router のミドルウェア構造とファイル規則
- 地理位置情報: 国、地域、言語によるルーティング
- 永続的な Cookie による安定した A/B テスト
- サーバーとのラウンドトリップを行わずにエッジでフラグを機能させる
- ミドルウェアでの軽量認証
- Node.js と比較した Vercel Edge ランタイムの制限
- ローカルミドルウェアのデバッグ
next dev
ファイル middleware.ts
Next.js (App Router および Pages Router) では、ミドルウェアはファイルで定義されます。
middleware.ts (o .js) プロジェクトルートにあります。
エクスポートされた関数はオブジェクトを受け取ります NextRequest そしてお返しをしなければなりません
ある NextResponse:
// middleware.ts - struttura base
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// La funzione middleware e chiamata per ogni richiesta che corrisponde al matcher
export function middleware(request: NextRequest): NextResponse {
// request.nextUrl: URL della richiesta (con searchParams, pathname, etc.)
// request.headers: header HTTP
// request.cookies: cookie della richiesta
// request.geo: geolocalizzazione (solo su Vercel, non in local dev)
// request.ip: IP del client
const response = NextResponse.next(); // Prosegui senza modifiche
// Aggiunge un header custom alla risposta
response.headers.set('X-Middleware-Applied', 'true');
return response;
}
// Configura su quali path il middleware viene eseguito
// IMPORTANTE: specificare il matcher migliora le performance
// evitando l'esecuzione per asset statici (_next/static, immagini, etc.)
export const config = {
matcher: [
// Esegui su tutte le route eccetto quelle statiche e API
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
地理位置情報: 国と言語によるルーティング
Vercel は、件名を介して各リクエストに地理情報を付加します。
request.geo。実稼働環境 (ローカル開発環境ではない) では利用可能です
国、地域、都市:
// middleware.ts - redirect geografico per lingua
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// Mappa paese -> locale preferito
const COUNTRY_LOCALE_MAP: Record<string, string> = {
IT: 'it',
DE: 'de',
FR: 'fr',
ES: 'es',
PT: 'pt',
BR: 'pt-BR',
// Default: 'en' per tutti gli altri paesi
};
const SUPPORTED_LOCALES = ['en', 'it', 'de', 'fr', 'es', 'pt'];
const DEFAULT_LOCALE = 'en';
export function middleware(request: NextRequest): NextResponse {
const { pathname } = request.nextUrl;
// Non processare le route che gia hanno un locale nel path
// Esempio: /it/dashboard non deve essere reindirizzato
const pathLocale = pathname.split('/')[1];
if (SUPPORTED_LOCALES.includes(pathLocale)) {
return NextResponse.next();
}
// Determina il locale preferito
// Priorita: 1) Cookie impostato dall'utente, 2) Paese IP, 3) Accept-Language, 4) Default
const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale)) {
return rewriteWithLocale(request, cookieLocale);
}
const country = request.geo?.country ?? 'US';
const geoLocale = COUNTRY_LOCALE_MAP[country] ?? DEFAULT_LOCALE;
// Verifica Accept-Language come fallback
const acceptLanguage = request.headers.get('Accept-Language') ?? '';
const browserLocale = parseAcceptLanguage(acceptLanguage, SUPPORTED_LOCALES);
const finalLocale = geoLocale !== DEFAULT_LOCALE
? geoLocale
: (browserLocale ?? DEFAULT_LOCALE);
return rewriteWithLocale(request, finalLocale);
}
function rewriteWithLocale(request: NextRequest, locale: string): NextResponse {
const url = request.nextUrl.clone();
url.pathname = `/${locale}${url.pathname}`;
// Rewrite (non redirect) per mantenere l'URL originale nel browser
// L'utente vede /dashboard ma viene servita /it/dashboard
return NextResponse.rewrite(url);
}
function parseAcceptLanguage(header: string, supported: string[]): string | null {
// Parsa "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7"
const languages = header
.split(',')
.map((lang) => {
const [code, q] = lang.trim().split(';q=');
return { code: code.split('-')[0].toLowerCase(), q: parseFloat(q ?? '1') };
})
.sort((a, b) => b.q - a.q);
for (const { code } of languages) {
if (supported.includes(code)) return code;
}
return null;
}
export const config = {
matcher: ['/((?!_next|api|favicon.ico|.*\\.\\w+).*)'],
};
Cookie を使用した安定した A/B テスト
エッジでの A/B テストでは、各ユーザーが独自のグループに留まる必要があります セッション全体(またはそれ以上)。標準的な解決策は、 最初のログイン時にグループを割り当て、その選択を Cookie に保存します。
// middleware.ts - A/B testing per homepage
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const AB_COOKIE = 'ab_homepage';
const AB_VARIANTS = ['control', 'variant_a', 'variant_b'] as const;
type AbVariant = typeof AB_VARIANTS[number];
// Distribuzione del traffico: 50% control, 25% variant_a, 25% variant_b
const WEIGHTS = [0.5, 0.25, 0.25];
export function middleware(request: NextRequest): NextResponse {
const { pathname } = request.nextUrl;
// Applica A/B test solo sulla homepage
if (pathname !== '/') {
return NextResponse.next();
}
const response = NextResponse.next();
// Controlla se l'utente ha gia un gruppo assegnato
let variant = request.cookies.get(AB_COOKIE)?.value as AbVariant | undefined;
if (!variant || !AB_VARIANTS.includes(variant as AbVariant)) {
// Assegna un nuovo gruppo deterministicamente
variant = assignVariant(request);
// Salva il gruppo nel cookie (1 mese di durata)
response.cookies.set(AB_COOKIE, variant, {
maxAge: 30 * 24 * 60 * 60, // 30 giorni
path: '/',
httpOnly: false, // Visibile a JavaScript per il tracking
sameSite: 'lax',
});
}
// Aggiunge il gruppo come header per il server component e analytics
response.headers.set('X-AB-Variant', variant);
// Rewrite verso la variante corretta
if (variant !== 'control') {
const url = request.nextUrl.clone();
url.pathname = `/experiments/homepage/${variant}`;
return NextResponse.rewrite(url, { headers: response.headers });
}
return response;
}
function assignVariant(request: NextRequest): AbVariant {
// Usa l'IP + User-Agent per un'assegnazione deterministica (non cambia tra ricariche)
const seed = `${request.ip ?? ''}${request.headers.get('User-Agent') ?? ''}`;
const hash = simpleHash(seed);
const normalized = (hash % 1000) / 1000; // Normalizza a [0, 1)
let cumulative = 0;
for (let i = 0; i < WEIGHTS.length; i++) {
cumulative += WEIGHTS[i];
if (normalized < cumulative) return AB_VARIANTS[i];
}
return AB_VARIANTS[0]; // Fallback al control
}
function simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0; // Converte a 32-bit integer
}
return Math.abs(hash);
}
export const config = {
matcher: ['/'],
};
エッジの機能フラグ
機能フラグを使用すると、サブセットの機能を有効にすることができます 新しい展開を行わないユーザーの数。エッジミドルウェアを使用すると、フラグを読み取ることができます エッジ設定 (Vercel Edge Config) から取得し、レンダリング前に適用します。
// middleware.ts - feature flags con Vercel Edge Config
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { get } from '@vercel/edge-config';
interface FeatureFlags {
newDashboard: boolean;
betaFeatures: string[]; // Lista di user IDs con accesso beta
maintenanceMode: boolean;
}
export async function middleware(request: NextRequest): Promise<NextResponse> {
// Edge Config ha latenza < 1ms (replicated globally)
// get() e sincrono in termini di latenza percepita
const flags = await get<FeatureFlags>('featureFlags');
// Modalita manutenzione globale
if (flags?.maintenanceMode) {
const url = request.nextUrl.clone();
url.pathname = '/maintenance';
return NextResponse.rewrite(url);
}
// Reindirizza alla nuova dashboard se il flag e attivo
if (flags?.newDashboard && request.nextUrl.pathname === '/dashboard') {
const url = request.nextUrl.clone();
url.pathname = '/dashboard-v2';
return NextResponse.rewrite(url);
}
// Accesso beta: controlla se l'utente e nella lista
const userId = request.cookies.get('user_id')?.value;
if (userId && flags?.betaFeatures?.includes(userId)) {
const response = NextResponse.next();
response.headers.set('X-Beta-User', 'true');
return response;
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next|api|.*\\.\\w+).*)'],
};
ミドルウェアでの軽量認証
ミドルウェアは、JWT を検証してリダイレクトするのに最適な場所です リクエストがサーバーに到達する前に、認証されていないユーザー:
// middleware.ts - verifica JWT leggera all'edge
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PUBLIC_PATHS = ['/login', '/register', '/api/auth', '/_next', '/favicon.ico'];
const AUTH_COOKIE = 'session_token';
export async function middleware(request: NextRequest): Promise<NextResponse> {
const { pathname } = request.nextUrl;
// Salta le route pubbliche
if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
return NextResponse.next();
}
const token = request.cookies.get(AUTH_COOKIE)?.value;
if (!token) {
return redirectToLogin(request);
}
try {
// Verifica il JWT usando Web Crypto API (disponibile nell'edge runtime)
const isValid = await verifyJwt(token, request.headers.get('x-jwt-secret') ?? '');
if (!isValid) {
return redirectToLogin(request);
}
return NextResponse.next();
} catch {
return redirectToLogin(request);
}
}
async function verifyJwt(token: string, secret: string): Promise<boolean> {
try {
const [headerB64, payloadB64, signatureB64] = token.split('.');
if (!headerB64 || !payloadB64 || !signatureB64) return false;
// Verifica la firma con HMAC-SHA256
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const signature = base64UrlDecode(signatureB64);
const data = encoder.encode(`${headerB64}.${payloadB64}`);
const isValid = await crypto.subtle.verify('HMAC', key, signature, data);
if (!isValid) return false;
// Verifica la scadenza
const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));
if (payload.exp && payload.exp < Date.now() / 1000) return false;
return true;
} catch {
return false;
}
}
function base64UrlDecode(str: string): ArrayBuffer {
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64 + '=='.slice(0, (4 - base64.length % 4) % 4);
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
function redirectToLogin(request: NextRequest): NextResponse {
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('callbackUrl', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.\\w+).*)'],
};
Vercel Edge ランタイムの制限事項
Vercel Edge ランタイムは、Node.js API のサブセットを備えた V8 に基づいています。 注意すべき主な制限事項は次のとおりです。
| 特性 | エッジランタイム | Node.js ランタイム |
|---|---|---|
| コールドスタート | < 5ms | ~50-500ms |
| 最大タイムアウト | 30 秒 (Vercel Pro の場合) | 15 分 (Vercel Enterprise の場合) |
| 最大メモリ | 128MB | 3GB |
| fs モジュール (ファイルシステム) | 利用不可 | 利用可能 |
| Node.js ビルトイン (パス、暗号ノード) | 部分的 | 完了 |
| ウェブ暗号化API | 利用可能 | 使用可能(ノード17から) |
| APIを取得する | ネイティブ | ネイティブ (ノード 18 から) |
| ネイティブバインディングを備えたnpmパッケージ | サポートされていません | サポートされています |
Edge ランタイムと互換性のないライブラリ
一般的な npm ライブラリの多くは Vercel Edge ランタイムと互換性がありません。
Prisma (ネイティブ ドライバーを使用)、bcrypt (ネイティブ バインディング)、sharp (ネイティブ イメージ)。
ミドルウェアでライブラリを使用する前に、それがエッジ ランタイムをサポートしていることを確認してください。
Prisma にはエッジ互換アダプター (@prisma/adapter-pg con
@neondatabase/serverless)、すべての機能が利用できるわけではありません。
ミドルウェアのデバッグとテスト
ミドルウェアはローカルでテストできます。 next dev。
いくつかの制限: request.geo それはいつもそうだ undefined
ローカルで (モックを使用)、Edge Config には環境変数が必要です
EDGE_CONFIG:
// middleware.ts con mock per development locale
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
function getGeo(request: NextRequest): { country?: string; region?: string } {
// In development, simula la geolocalizzazione tramite header custom
// Utile per testare diverse regioni senza deploy
if (process.env.NODE_ENV === 'development') {
return {
country: request.headers.get('X-Mock-Country') ?? 'US',
region: request.headers.get('X-Mock-Region') ?? 'CA',
};
}
return {
country: request.geo?.country,
region: request.geo?.region,
};
}
export function middleware(request: NextRequest): NextResponse {
const { country, region } = getGeo(request);
// Logica di routing geo-based
const response = NextResponse.next();
response.headers.set('X-Country', country ?? 'unknown');
response.headers.set('X-Region', region ?? 'unknown');
return response;
}
結論と次のステップ
Vercel ミドルウェアは、最も強力なツールの 1 つです。 Next.js アプリケーションの動作を変更せずにカスタマイズする サーバー側のロジック。レンダリング前のエッジでの位置 およびキャッシュ — ルーティングを迅速に決定するのに最適です。 データベースへのアクセスは必要ありません。
表示されるパターン (地理位置情報、A/B テスト、機能フラグ、認証) 一般的な使用例のほとんどをカバーしています。成功の鍵は ミドルウェアを軽量に保つ: 複雑なロジックまたは遅いデータベース アクセス サーバーコンポーネントまたはルートハンドラーに属します。
シリーズの次の記事
- 第7条: エッジでの地理的ルーティング — パーソナライゼーション コンテンツとGDPRコンプライアンス: Cloudflare Workersによるジオフェンシング、ローカライズされた価格設定 地域の規制によりブロックされます。
- 第8条: CloudflareのキャッシュAPIと無効化戦略 ワーカー: 本番環境での再検証中の失効とキーによる無効化。
- 第9条: 地元の労働者の検査 — Miniflare、Vitest Wrangler Dev: 導入を必要としないテストのための完全なワークフロー。







