Vercel Edge Runtime: Pokročilý middleware, geolokace a A/B testování
S Vercelem provádí okrajový middleware nejprve vykreslování SSR a umožňuje směrování na základě geolokace, příznaků funkcí, A/B testování a ověřování bez studeného startu, to vše v celosvětovém průměru za méně než 50 ms.
Middleware před vykreslením
V tradiční aplikaci Next.js nasazené na Vercelu každý požadavek nejprve dosáhne CDN a poté – pokud není v mezipaměti – je předán dál na server SSR. Mezi tyto dvě fáze zapadá Edge middleware: přichází provedené na okraji sítě Vercel před směrováním na server SSR a předtím, než je odpověď doručena z mezipaměti.
Toto umístění je kritické: middleware se může změnit požadavek, přesměrování uživatele, nastavení cookies nebo hlaviček a tak dále se děje v běhovém prostředí založeném na V8 se studeným startem blízkým nule. Vercel měří latence P50 < 10 ms pro většinu middlewaru v jejich okrajové oblasti (100+ celosvětově).
Co se naučíte
- Struktura middlewaru a konvence souborů v Next.js App Router
- Geolokace: Směrování podle země, regionu a jazyka
- Stabilní A/B testování s trvalými soubory cookie
- Příznaky funkce na okraji bez okružních cest na server
- Lehká autentizace v middlewaru
- Omezení Vercel Edge Runtime ve srovnání s Node.js
- Lokální ladění middlewaru pomocí
next dev
Soubor middleware.ts
V Next.js (App Router a Pages Router) je middleware definován v souboru
middleware.ts (o .js) v kořenovém adresáři projektu.
Exportovaná funkce obdrží objekt NextRequest a musí vrátit
a 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)$).*)',
],
};
Geolokace: Směrování podle země a jazyka
Vercel obohacuje každý požadavek o geografické informace prostřednictvím předmětu
request.geo. Ve výrobě (ne v místním vývoji) jsou k dispozici
země, region a město:
// 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+).*)'],
};
Stabilní A/B testování s cookies
A/B testování na hranici vyžaduje, aby každý uživatel zůstal ve své vlastní skupině po celou dobu sezení (nebo déle). Standardní řešení je přiřadit skupinu při prvním přihlášení a ponechat volbu v souboru 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: ['/'],
};
Vlajky funkcí na okraji
Příznaky funkcí umožňují povolit funkce pro podmnožiny uživatelů bez nového nasazení. S edge middlewarem můžete číst příznaky z konfigurace okraje (Vercel Edge Config) a použijte je před vykreslením:
// 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+).*)'],
};
Lehká autentizace v Middleware
Middleware je skvělé místo pro ověření JWT a přesměrování neověření uživatelé před tím, než požadavek dorazí na server:
// 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+).*)'],
};
Omezení Vercel Edge Runtime
Vercel Edge Runtime je založen na V8 s podmnožinou Node.js API. Hlavní omezení, která je třeba si uvědomit:
| Charakteristický | Edge Runtime | Node.js Runtime |
|---|---|---|
| Studený start | < 5 ms | ~50-500 ms |
| Maximální časový limit | 30s (na Vercel Pro) | 15 minut (na Vercel Enterprise) |
| Maximální paměť | 128 MB | 3 GB |
| fs modul (systém souborů) | Není k dispozici | K dispozici |
| Vestavěné prvky Node.js (cesta, kryptografický uzel) | Částečný | Kompletní |
| Web Crypto API | K dispozici | K dispozici (od uzlu 17) |
| Načíst API | Rodák | Nativní (z uzlu 18) |
| npm balíčky s nativními vazbami | Není podporováno | Podporováno |
Knihovny nekompatibilní s Edge Runtime
Mnoho populárních knihoven npm není kompatibilních s Vercel Edge Runtime:
Prisma (používá nativní ovladače), bcrypt (nativní vazba), ostrý (nativní obrázky).
Než použijete knihovnu v middlewaru, ověřte, zda podporuje hraniční běhové prostředí.
Prisma má adaptér kompatibilní s edge (@prisma/adapter-pg con
@neondatabase/serverless), ale ne všechny funkce jsou k dispozici.
Ladění a testování middlewaru
Middleware je testovatelný lokálně pomocí next dev.
Některá omezení: request.geo to je vždycky undefined
lokálně (použijte mock) a Edge Config vyžaduje proměnnou prostředí
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;
}
Závěry a další kroky
Middleware Vercel je jedním z nejmocnějších nástrojů pro přizpůsobit chování aplikace Next.js bez úprav logika na straně serveru. Jeho poloha na okraji — před vykreslením a mezipaměť – je ideální pro rychlá rozhodnutí o směrování nevyžadují přístup k databázi.
Zobrazené vzory (geolokalizace, A/B testování, příznaky funkcí, ověřování) pokrývají většinu běžných případů použití. Klíčem k úspěchu je udržovat middleware lehký: složitá logika nebo pomalé přístupy k databázi patří do Server Components nebo Route Handlers.
Další články v seriálu
- Článek 7: Geografické směrování na okraji — Personalizace Obsah a soulad s GDPR: geo-fencing s Cloudflare Workers, lokalizované ceny a zablokovat z důvodu místních předpisů.
- Článek 8: Cache API a strategie zneplatnění v Cloudflare Pracovníci: zatuchlý-během-revalidace a zneplatnění klíčem v produkci.
- Článek 9: Testování pracovníků v Local — Miniflare, Vitest a Wrangler Dev: kompletní pracovní postup pro testování bez nasazení.







