Vercel Edge Runtime: zaawansowane oprogramowanie pośredniczące, geolokalizacja i testy A/B
W przypadku Vercel oprogramowanie pośredniczące na krawędzi najpierw wykonuje renderowanie SSR, umożliwiając routing w oparciu o geolokalizację, flagi funkcji, testy A/B i uwierzytelnianie bez zimnego startu, a wszystko to w czasie krótszym niż 50 ms, średnio na całym świecie.
Oprogramowanie pośrednie przed renderowaniem
W tradycyjnej aplikacji Next.js wdrożonej na platformie Vercel każde żądanie najpierw dociera do CDN, a następnie — jeśli nie znajduje się w pamięci podręcznej — jest przekazywany dalej do serwera SSR. Oprogramowanie pośrednie Edge mieści się pomiędzy tymi dwoma etapami: przychodzi wykonywane na brzegu sieci Vercel, przed routingiem do serwera SSR i przed dostarczeniem odpowiedzi z pamięci podręcznej.
To umiejscowienie jest krytyczne: oprogramowanie pośrednie może modyfikować żądanie, przekierowanie użytkownika, ustawienie plików cookie lub nagłówków i to wszystko dzieje się w środowisku wykonawczym opartym na V8 z zimnym startem bliskim zera. środki Vercela opóźnienie P50 < 10 ms dla większości oprogramowania pośredniego w ich regiony brzegowe (ponad 100 na całym świecie).
Czego się nauczysz
- Struktura oprogramowania pośredniczącego i konwencja plików w routerze aplikacji Next.js
- Geolokalizacja: wyznaczanie trasy według kraju, regionu i języka
- Stabilne testy A/B z trwałymi plikami cookie
- Flagi funkcyjne na krawędzi bez podróży w obie strony do serwera
- Lekkie uwierzytelnianie w oprogramowaniu pośrednim
- Ograniczenia Vercel Edge Runtime w porównaniu z Node.js
- Lokalne debugowanie oprogramowania pośredniego za pomocą
next dev
Plik middleware.ts
W Next.js (App Router i Pages Router) oprogramowanie pośredniczące jest zdefiniowane w pliku
middleware.ts (o .js) w katalogu głównym projektu.
Eksportowana funkcja otrzymuje obiekt NextRequest i musi oddać
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)$).*)',
],
};
Geolokalizacja: wyznaczanie trasy według kraju i języka
Vercel wzbogaca każde zapytanie o informacje geograficzne poprzez temat
request.geo. W produkcji (nie u lokalnego dewelopera) są dostępne
kraj, region i miasto:
// 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+).*)'],
};
Stabilne testy A/B z plikami cookie
Testowanie A/B na brzegu wymaga od każdego użytkownika pozostania w swojej własnej grupie przez całą sesję (lub dłużej). Standardowym rozwiązaniem jest przypisz grupę przy pierwszym logowaniu i zapisz wybór w pliku 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: ['/'],
};
Flagi funkcyjne na krawędzi
Flagi funkcji umożliwiają włączenie funkcji dla podzbiorów użytkowników bez nowego wdrożenia. Dzięki oprogramowaniu pośredniczącemu Edge można odczytywać flagi z konfiguracji brzegowej (Vercel Edge Config) i zastosuj je przed renderowaniem:
// 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+).*)'],
};
Lekkie uwierzytelnianie w oprogramowaniu pośrednim
Oprogramowanie pośrednie to świetne miejsce do weryfikacji JWT i przekierowania nieuwierzytelnieni użytkownicy, zanim żądanie dotrze do serwera:
// 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+).*)'],
};
Ograniczenia środowiska wykonawczego Vercel Edge
Środowisko wykonawcze Vercel Edge jest oparte na wersji V8 z podzbiorem interfejsu API Node.js. Główne ograniczenia, o których należy pamiętać:
| Charakterystyczny | Środowisko wykonawcze Edge | Środowisko wykonawcze Node.js |
|---|---|---|
| Zimny start | < 5 ms | ~50-500ms |
| Maksymalny limit czasu | 30s (na Vercel Pro) | 15 minut (w programie Vercel Enterprise) |
| Maksymalna pamięć | 128MB | 3 GB |
| moduł fs (system plików) | Niedostępne | Dostępny |
| Wbudowane rozwiązania Node.js (ścieżka, węzeł kryptograficzny) | Częściowy | Kompletny |
| Internetowy interfejs API kryptowalut | Dostępny | Dostępne (od węzła 17) |
| Pobierz API | Rodzinny | Natywny (z węzła 18) |
| pakiety npm z natywnymi powiązaniami | Nieobsługiwane | Utrzymany |
Biblioteki niezgodne z Edge Runtime
Wiele popularnych bibliotek npm nie jest kompatybilnych ze środowiskiem wykonawczym Vercel Edge:
Prisma (używa sterowników natywnych), bcrypt (powiązanie natywne), Sharp (obrazy natywne).
Przed użyciem biblioteki w oprogramowaniu pośrednim sprawdź, czy obsługuje ona środowisko wykonawcze brzegowe.
Prisma ma adapter kompatybilny z krawędziami (@prisma/adapter-pg con
@neondatabase/serverless), ale nie wszystkie funkcje są dostępne.
Debugowanie i testowanie oprogramowania pośredniego
Oprogramowanie pośrednie można testować lokalnie za pomocą next dev.
Niektóre ograniczenia: request.geo zawsze tak jest undefined
lokalnie (użyj próby), a Edge Config wymaga zmiennej środowiskowej
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;
}
Wnioski i dalsze kroki
Oprogramowanie pośrednie Vercel jest jednym z najpotężniejszych narzędzi do dostosuj zachowanie aplikacji Next.js bez modyfikacji logika po stronie serwera. Jego położenie na krawędzi — przed renderowaniem i pamięć podręczna — sprawia, że idealnie nadaje się do szybkich decyzji dotyczących routingu nie wymagają dostępu do bazy danych.
Pokazane wzorce (gelokalizacja, testy A/B, flagi funkcji, uwierzytelnianie) obejmują większość typowych przypadków użycia. Kluczem do sukcesu jest zachowaj lekkość oprogramowania pośredniego: złożona logika lub powolny dostęp do bazy danych należą do Komponentów Serwera lub Procedur Obsługi Tras.
Następne artykuły z serii
- Artykuł 7: Trasowanie geograficzne na krawędzi — personalizacja Zgodność z treścią i RODO: geofencing z pracownikami Cloudflare, zlokalizowane ceny i zablokować zgodnie z lokalnymi przepisami.
- Artykuł 8: Cache API i strategie unieważniania w Cloudflare Pracownicy: nieaktualne podczas ponownej walidacji i unieważnianie według klucza w produkcji.
- Artykuł 9: Testowanie pracowników lokalnych — Miniflare, Vitest i Wrangler Dev: kompletny przepływ pracy do testowania bez wdrażania.







