Edge Functions : ce qu'elles sont, quand les utiliser et quand s'abstenir
Edge runtime, V8 isolates, le mythe du cold start, geo-routing, A/B testing, auth à l'edge et pourquoi j'ai ramené certaines choses sur Node.js. Un regard équilibré sur l'edge computing.
Le mot « edge » est utilisé à toutes les sauces. Vercel le dit. Cloudflare le dit. Deno le dit. Chaque conférence sur la performance web mentionne inévitablement « l'exécution à l'edge » comme une incantation magique qui rend votre application rapide.
J'y ai cru. J'ai déplacé des middlewares, des routes API, et même de la logique de rendu vers l'edge runtime. Certains de ces choix étaient brillants. D'autres, je les ai discrètement ramenés sur Node.js trois semaines plus tard, après avoir débogué des erreurs de pool de connexions à 2 heures du matin.
Cet article est la version équilibrée de cette histoire — ce qu'est réellement l'edge, où il brille véritablement, où il ne brille absolument pas, et comment je décide quel runtime utiliser pour chaque partie de mon application.
Qu'est-ce que l'edge ?#
Commençons par la géographie. Quand quelqu'un visite votre site web, sa requête voyage de son appareil, à travers son FAI, à travers Internet jusqu'à votre serveur, est traitée, et la réponse fait tout le chemin en sens inverse. Si votre serveur est en us-east-1 (Virginie) et que votre utilisateur est à Tokyo, cet aller-retour couvre environ 14 000 km. À la vitesse de la lumière dans la fibre optique, c'est environ 70 ms rien que pour la physique — dans un sens. Ajoutez la résolution DNS, la poignée de main TLS et le temps de traitement, et vous atteignez facilement 200-400 ms avant que votre utilisateur ne voie un seul octet.
L'« edge » signifie exécuter votre code sur des serveurs distribués mondialement — les mêmes nœuds CDN qui ont toujours servi des assets statiques, mais qui peuvent maintenant aussi exécuter votre logique. Au lieu d'un seul serveur d'origine en Virginie, votre code s'exécute dans plus de 300 emplacements dans le monde. Un utilisateur à Tokyo touche un serveur à Tokyo. Un utilisateur à Paris touche un serveur à Paris.
Le calcul de latence est simple et convaincant :
Traditionnel (origine unique) :
Tokyo → Virginie : ~140 ms aller-retour (physique seule)
+ Poignée de main TLS : ~140 ms de plus (un autre aller-retour)
+ Traitement : 20-50 ms
Total : ~300-330 ms
Edge (PoP local) :
Tokyo → nœud edge à Tokyo : ~5 ms aller-retour
+ Poignée de main TLS : ~5 ms de plus
+ Traitement : 5-20 ms
Total : ~15-30 ms
C'est une amélioration de 10 à 20 fois pour la réponse initiale. C'est réel, c'est mesurable, et pour certaines opérations c'est transformateur.
Mais voici ce que le marketing passe sous silence : l'edge n'est pas un environnement serveur complet. C'est quelque chose de fondamentalement différent.
V8 Isolates vs Node.js#
Node.js traditionnel tourne dans un processus complet du système d'exploitation. Il a accès au système de fichiers, il peut ouvrir des connexions TCP, il peut lancer des processus enfants, il peut lire des variables d'environnement en flux, il peut faire essentiellement tout ce qu'un processus Linux peut faire.
Les edge functions ne tournent pas sur Node.js. Elles tournent sur des V8 isolates — le même moteur JavaScript qui fait tourner Chrome, mais réduit à l'essentiel. Pensez à un V8 isolate comme un bac à sable léger :
// Cela fonctionne dans Node.js mais PAS à l'edge
import fs from "fs";
import { createConnection } from "net";
import { execSync } from "child_process";
const file = fs.readFileSync("/etc/hosts"); // ❌ Pas de système de fichiers
const conn = createConnection({ port: 5432 }); // ❌ Pas de TCP brut
const result = execSync("ls -la"); // ❌ Pas de processus enfants
process.env.DATABASE_URL; // ⚠️ Disponible mais statique, défini au déploiementCe que vous AVEZ à l'edge, c'est la surface API Web — les mêmes APIs disponibles dans un navigateur :
// Tout cela fonctionne à l'edge
const response = await fetch("https://api.example.com/data");
const url = new URL(request.url);
const headers = new Headers({ "Content-Type": "application/json" });
const encoder = new TextEncoder();
const encoded = encoder.encode("hello");
const hash = await crypto.subtle.digest("SHA-256", encoded);
const id = crypto.randomUUID();
// API Web Streams
const stream = new ReadableStream({
start(controller) {
controller.enqueue("chunk 1");
controller.enqueue("chunk 2");
controller.close();
},
});
// API Cache
const cache = caches.default;
await cache.put(request, response.clone());Les contraintes sont réelles et strictes :
- Mémoire : 128 Mo par isolate (Cloudflare Workers), 256 Mo sur certaines plateformes
- Temps CPU : 10-50 ms de temps CPU réel (pas le temps horloge —
await fetch()ne compte pas, maisJSON.parse()sur un payload de 5 Mo, oui) - Pas de modules natifs : Tout ce qui nécessite un binding C++ (bcrypt, sharp, canvas) est exclu
- Pas de connexions persistantes : Vous ne pouvez pas maintenir une connexion base de données ouverte entre les requêtes
- Limites de taille du bundle : Typiquement 1-5 Mo pour l'intégralité du script worker
Ce n'est pas Node.js sur un CDN. C'est un runtime différent avec un modèle mental différent.
Cold starts : le mythe et la réalité#
Vous avez probablement entendu que les edge functions ont « zéro cold start ». C'est... en grande partie vrai, et la comparaison est véritablement spectaculaire.
Une fonction serverless traditionnelle basée sur des conteneurs (AWS Lambda, Google Cloud Functions) fonctionne comme ceci :
- La requête arrive
- La plateforme provisionne un conteneur (si aucun n'est disponible)
- Le conteneur démarre l'OS
- Le runtime s'initialise (Node.js, Python, etc.)
- Votre code se charge et s'initialise
- La requête est traitée
Les étapes 2-5 constituent le cold start. Pour une Lambda Node.js, c'est typiquement 200-500 ms. Pour une Lambda Java, ça peut être 2-5 secondes. Pour une Lambda .NET, 500 ms-1,5 s.
Les V8 isolates fonctionnent différemment :
- La requête arrive
- La plateforme crée un nouveau V8 isolate (ou réutilise un qui est chaud)
- Votre code se charge (il est déjà compilé en bytecode au moment du déploiement)
- La requête est traitée
Les étapes 2-3 prennent moins de 5 ms. Souvent moins de 1 ms. L'isolate n'est pas un conteneur — il n'y a pas d'OS à démarrer, pas de runtime à initialiser. V8 crée un isolate frais en microsecondes. L'expression « zéro cold start » est du marketing, mais la réalité (démarrage sub-5 ms) est suffisamment proche de zéro pour que ça n'ait pas d'importance dans la plupart des cas d'utilisation.
Mais voici quand les cold starts posent encore problème à l'edge :
Gros bundles. Si votre edge function charge 2 Mo de dépendances, ce code doit encore être chargé et parsé. J'ai appris cela à mes dépens quand j'ai intégré une bibliothèque de validation et une bibliothèque de formatage de dates dans un edge middleware. Le cold start est passé de 2 ms à 40 ms. Toujours rapide, mais pas « zéro ».
Emplacements rares. Les fournisseurs edge ont des centaines de PoPs, mais tous les PoPs ne gardent pas votre code chaud. Si vous recevez une requête par heure depuis Nairobi, cet isolate est recyclé entre les requêtes. La requête suivante paie à nouveau le coût de démarrage.
Multiples isolates par requête. Si votre edge function appelle une autre edge function (ou si le middleware et une route API sont tous deux edge), vous pourriez lancer plusieurs isolates pour une seule requête utilisateur.
Le conseil pratique : gardez vos bundles d'edge functions petits. N'importez que ce dont vous avez besoin. Tree-shakez agressivement. Plus le bundle est petit, plus le cold start est rapide, plus la promesse de « zéro cold start » tient la route.
// ❌ Ne faites pas cela à l'edge
import dayjs from "dayjs";
import * as yup from "yup";
import lodash from "lodash";
// ✅ Faites plutôt ceci — utilisez les APIs natives
const date = new Date().toISOString();
const isValid = typeof input === "string" && input.length < 200;
const unique = [...new Set(items)];Cas d'utilisation parfaits pour les edge functions#
Après avoir beaucoup expérimenté, j'ai trouvé un pattern clair : les edge functions excellent quand vous devez prendre une décision rapide sur une requête avant qu'elle n'atteigne votre serveur d'origine. Ce sont des gardiens, des routeurs et des transformateurs — pas des serveurs d'application.
1. Redirections basées sur la géolocalisation#
C'est le cas d'utilisation phare. La requête touche le nœud edge le plus proche, qui sait déjà où se trouve l'utilisateur. Pas besoin d'appel API, pas de base de données de recherche d'IP — la plateforme fournit les données de géolocalisation :
// middleware.ts — s'exécute à l'edge sur chaque requête
import { NextRequest, NextResponse } from "next/server";
export const config = {
matcher: ["/", "/shop/:path*"],
};
export function middleware(request: NextRequest) {
const country = request.geo?.country ?? "US";
const city = request.geo?.city ?? "Unknown";
const region = request.geo?.region ?? "Unknown";
// Rediriger vers la boutique spécifique au pays
if (request.nextUrl.pathname === "/shop") {
const storeMap: Record<string, string> = {
DE: "/shop/eu",
FR: "/shop/eu",
GB: "/shop/uk",
JP: "/shop/jp",
TR: "/shop/tr",
};
const storePath = storeMap[country] ?? "/shop/us";
if (request.nextUrl.pathname !== storePath) {
return NextResponse.redirect(new URL(storePath, request.url));
}
}
// Ajouter les en-têtes géo pour une utilisation en aval
const response = NextResponse.next();
response.headers.set("x-user-country", country);
response.headers.set("x-user-city", city);
response.headers.set("x-user-region", region);
return response;
}Cela s'exécute en moins de 5 ms, juste à côté de l'utilisateur. L'alternative — envoyer la requête jusqu'à votre serveur d'origine juste pour faire une recherche d'IP et rediriger — coûterait 100-300 ms pour les utilisateurs éloignés de votre origine.
2. A/B testing sans scintillement côté client#
L'A/B testing côté client provoque le redouté « flash du contenu original » — l'utilisateur voit la version A pendant une fraction de seconde avant que JavaScript ne permute vers la version B. À l'edge, vous pouvez assigner la variante avant même que la page ne commence à se rendre :
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Vérifier si l'utilisateur a déjà une attribution de variante
const existingVariant = request.cookies.get("ab-variant")?.value;
if (existingVariant) {
// Réécrire vers la page de la bonne variante
const url = request.nextUrl.clone();
url.pathname = `/variants/${existingVariant}${url.pathname}`;
return NextResponse.rewrite(url);
}
// Attribuer une nouvelle variante (répartition 50/50)
const variant = Math.random() < 0.5 ? "control" : "treatment";
const url = request.nextUrl.clone();
url.pathname = `/variants/${variant}${url.pathname}`;
const response = NextResponse.rewrite(url);
response.cookies.set("ab-variant", variant, {
maxAge: 60 * 60 * 24 * 30, // 30 jours
httpOnly: true,
sameSite: "lax",
});
return response;
}L'utilisateur ne voit jamais de scintillement parce que la réécriture se fait au niveau du réseau. Le navigateur ne sait même pas qu'il s'agissait d'un test A/B — il reçoit simplement la page de la variante directement.
3. Vérification de jeton d'authentification#
Si votre auth utilise des JWTs (et que vous ne faites pas de recherche de session en base de données), l'edge est parfait. La vérification JWT est du pur crypto — aucune base de données nécessaire :
import { jwtVerify, importSPKI } from "jose";
import { NextRequest, NextResponse } from "next/server";
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`;
export async function middleware(request: NextRequest) {
const token = request.cookies.get("session-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
try {
const key = await importSPKI(PUBLIC_KEY, "RS256");
const { payload } = await jwtVerify(token, key, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
});
// Transmettre les informations utilisateur en aval via les en-têtes
const response = NextResponse.next();
response.headers.set("x-user-id", payload.sub as string);
response.headers.set("x-user-role", payload.role as string);
return response;
} catch {
// Jeton expiré ou invalide
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("session-token");
return response;
}
}Ce pattern est puissant : le middleware edge vérifie le jeton et transmet les informations utilisateur à votre origine sous forme d'en-têtes de confiance. Vos routes API n'ont pas besoin de vérifier le jeton à nouveau — elles lisent simplement request.headers.get("x-user-id").
4. Détection de bots et limitation de débit#
Les edge functions peuvent bloquer le trafic indésirable avant qu'il n'atteigne jamais votre origine :
import { NextRequest, NextResponse } from "next/server";
// Simple limiteur de débit en mémoire (par emplacement edge)
const rateLimitMap = new Map<string, { count: number; timestamp: number }>();
export function middleware(request: NextRequest) {
const ip = request.headers.get("x-forwarded-for")?.split(",").pop()?.trim()
?? "unknown";
const ua = request.headers.get("user-agent") ?? "";
// Bloquer les bots malveillants connus
const badBots = ["AhrefsBot", "SemrushBot", "MJ12bot", "DotBot"];
if (badBots.some((bot) => ua.includes(bot))) {
return new NextResponse("Forbidden", { status: 403 });
}
// Limitation de débit simple
const now = Date.now();
const windowMs = 60_000; // 1 minute
const maxRequests = 100;
const entry = rateLimitMap.get(ip);
if (entry && now - entry.timestamp < windowMs) {
entry.count++;
if (entry.count > maxRequests) {
return new NextResponse("Too Many Requests", {
status: 429,
headers: { "Retry-After": "60" },
});
}
} else {
rateLimitMap.set(ip, { count: 1, timestamp: now });
}
// Nettoyage périodique pour éviter les fuites de mémoire
if (rateLimitMap.size > 10_000) {
const cutoff = now - windowMs;
for (const [key, val] of rateLimitMap) {
if (val.timestamp < cutoff) rateLimitMap.delete(key);
}
}
return NextResponse.next();
}Une mise en garde : la map de limitation de débit ci-dessus est par isolate, par emplacement. Si vous avez 300 emplacements edge, chacun a sa propre map. Pour une limitation de débit stricte, vous avez besoin d'un store distribué comme Upstash Redis ou Cloudflare Durable Objects. Mais pour une prévention approximative des abus, les limites par emplacement fonctionnent étonnamment bien.
5. Réécriture de requêtes et en-têtes de personnalisation#
Les edge functions sont excellentes pour transformer les requêtes avant qu'elles n'atteignent votre origine :
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const url = request.nextUrl;
// Négociation de contenu basée sur l'appareil
const ua = request.headers.get("user-agent") ?? "";
const isMobile = /mobile|android|iphone/i.test(ua);
response.headers.set("x-device-type", isMobile ? "mobile" : "desktop");
// Feature flags depuis le cookie
const flags = request.cookies.get("feature-flags")?.value;
if (flags) {
response.headers.set("x-feature-flags", flags);
}
// Détection de la locale pour l'i18n
const acceptLanguage = request.headers.get("accept-language") ?? "en";
const preferredLocale = acceptLanguage.split(",")[0]?.split("-")[0] ?? "en";
const supportedLocales = [
"en", "tr", "de", "fr", "es", "pt", "ja", "ko", "it",
"nl", "ru", "pl", "uk", "sv", "cs", "ar", "hi", "zh",
];
const locale = supportedLocales.includes(preferredLocale)
? preferredLocale
: "en";
if (!url.pathname.startsWith(`/${locale}`) && !url.pathname.startsWith("/api")) {
return NextResponse.redirect(new URL(`/${locale}${url.pathname}`, request.url));
}
return response;
}Où l'edge échoue#
C'est la section que les pages marketing passent sous silence. J'ai heurté chacun de ces murs.
1. Connexions à la base de données#
C'est le gros problème. Les bases de données traditionnelles (PostgreSQL, MySQL) utilisent des connexions TCP persistantes. Un serveur Node.js ouvre un pool de connexions au démarrage et réutilise ces connexions entre les requêtes. Efficace, éprouvé, bien compris.
Les edge functions ne peuvent pas faire ça. Chaque isolate est éphémère. Il n'y a pas de phase de « démarrage » où vous ouvrez des connexions. Même si vous pouviez ouvrir une connexion, l'isolate pourrait être recyclé après une seule requête, gaspillant le temps de mise en place de la connexion.
// ❌ Ce pattern ne fonctionne fondamentalement pas à l'edge
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // Pool de connexions de 10
});
// Chaque invocation edge va :
// 1. Créer un nouveau Pool (ne peut pas être réutilisé de manière fiable entre les invocations)
// 2. Ouvrir une connexion TCP vers votre base de données (qui est en us-east-1, pas à l'edge)
// 3. Faire la poignée de main TLS avec la base de données
// 4. Exécuter la requête
// 5. Jeter la connexion quand l'isolate est recyclé
// Même avec des services de pooling de connexions comme PgBouncer,
// vous payez toujours la latence réseau de l'edge → base de données d'origineLe problème d'aller-retour vers la base de données est fondamental. Votre base de données est dans une seule région. Votre edge function est dans 300 régions. Chaque requête de base de données depuis l'edge doit voyager de l'emplacement edge à la région de la base de données et retour. Pour un utilisateur à Tokyo touchant un nœud edge à Tokyo, mais votre base de données est en Virginie :
Edge function à Tokyo
→ Requête vers PostgreSQL en Virginie : ~140 ms aller-retour
→ Deuxième requête : ~140 ms de plus
→ Total : 280 ms juste pour deux requêtes
Fonction Node.js en Virginie (même région que la BDD)
→ Requête vers PostgreSQL : ~1 ms aller-retour
→ Deuxième requête : ~1 ms de plus
→ Total : 2 ms pour deux requêtes
L'edge function est 140 fois plus lente pour les opérations de base de données dans ce scénario. Peu importe que l'edge function ait démarré plus vite — les allers-retours vers la base de données dominent tout.
C'est pourquoi les proxys de base de données HTTP existent (le driver serverless de Neon, le driver basé sur fetch de PlanetScale, l'API REST de Supabase). Ils fonctionnent, mais ils font toujours des requêtes HTTP vers une base de données dans une seule région. Ils résolvent le problème « pas de TCP possible » mais pas le problème « la base de données est loin ».
// ✅ Cela fonctionne à l'edge (accès base de données via HTTP)
// Mais c'est toujours lent si la base de données est loin du nœud edge
import { neon } from "@neondatabase/serverless";
export const runtime = "edge";
export async function GET(request: Request) {
const sql = neon(process.env.DATABASE_URL!);
// Cela fait une requête HTTP vers votre base de données Neon
// Fonctionne, mais la latence dépend de la distance à la région de la base
const posts = await sql`SELECT * FROM posts WHERE published = true LIMIT 10`;
return Response.json(posts);
}2. Tâches longues#
Les edge functions ont des limites de temps CPU, typiquement 10-50 ms de temps de calcul réel. Le temps horloge est plus généreux (généralement 30 secondes), mais les opérations intensives en CPU atteindront la limite rapidement :
// ❌ Ceux-ci vont dépasser les limites de temps CPU à l'edge
export const runtime = "edge";
export async function POST(request: Request) {
const data = await request.json();
// Traitement d'image — intensif en CPU
// (Ne peut pas non plus utiliser sharp car c'est un module natif)
const processed = heavyImageProcessing(data.image);
// Génération de PDF — intensive en CPU + nécessite des APIs Node.js
const pdf = generatePDF(data.content);
// Transformation de données volumineuses
const result = data.items // 100 000 éléments
.map(transform)
.filter(validate)
.sort(compare)
.reduce(aggregate, {});
return Response.json(result);
}Si votre fonction a besoin de plus que quelques millisecondes de temps CPU, elle appartient à un serveur Node.js régional. Point final.
3. Dépendances exclusives à Node.js#
Celle-ci surprend beaucoup de gens. Un nombre étonnant de packages npm dépendent de modules intégrés de Node.js :
// ❌ Ces packages ne fonctionneront pas à l'edge
import bcrypt from "bcrypt"; // Binding C++ natif
import sharp from "sharp"; // Binding C++ natif
import puppeteer from "puppeteer"; // Nécessite filesystem + child_process
import nodemailer from "nodemailer"; // Nécessite le module net
import { readFile } from "fs/promises"; // API filesystem de Node.js
import mongoose from "mongoose"; // Connexions TCP + APIs Node.js
// ✅ Alternatives compatibles edge
import { hashSync } from "bcryptjs"; // Implémentation JS pure (plus lente)
// Pour les images : utilisez un service ou une API séparée
// Pour l'email : utilisez une API email HTTP (Resend, SendGrid REST)
// Pour la base de données : utilisez des clients HTTPAvant de déplacer quoi que ce soit vers l'edge, vérifiez chaque dépendance. Un require("fs") enfoui trois niveaux de profondeur dans votre arbre de dépendances fera planter votre edge function au runtime — pas au moment du build. Vous déploierez, tout aura l'air bien, puis la première requête qui touche ce chemin de code produira une erreur cryptique.
4. Bundles de grande taille#
Les plateformes edge ont des limites strictes de taille de bundle :
- Cloudflare Workers : 1 Mo (gratuit), 5 Mo (payant)
- Vercel Edge Functions : 4 Mo (compressé)
- Deno Deploy : 20 Mo
Cela semble suffisant jusqu'à ce que vous importiez une bibliothèque de composants UI, une bibliothèque de validation et une bibliothèque de dates. J'ai eu un edge middleware qui a gonflé à 3,5 Mo parce que j'avais importé depuis un fichier barrel qui tirait l'intégralité du répertoire @/components.
// ❌ Les imports depuis des fichiers barrel peuvent tirer beaucoup trop de code
import { validateEmail } from "@/lib/utils";
// Si utils.ts réexporte depuis 20 autres modules, tous sont bundlés
// ✅ Importez directement depuis la source
import { validateEmail } from "@/lib/validators/email";5. Streaming et WebSockets#
Les edge functions peuvent faire des réponses en streaming (API Web Streams), mais les connexions WebSocket de longue durée sont une autre histoire. Bien que certaines plateformes supportent les WebSockets à l'edge (Cloudflare Workers, Deno Deploy), la nature éphémère des edge functions en fait un mauvais choix pour les connexions avec état de longue durée.
Edge runtime de Next.js#
Next.js rend simple l'adoption de l'edge runtime route par route. Vous n'avez pas à tout migrer — vous choisissez exactement quelles routes s'exécutent à l'edge.
Middleware (toujours edge)#
Le middleware Next.js s'exécute toujours à l'edge. C'est par conception — le middleware intercepte chaque requête correspondante, il doit donc être rapide et distribué mondialement :
// middleware.ts — s'exécute toujours à l'edge, pas besoin d'opt-in
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Cela s'exécute avant chaque requête correspondante
// Gardez-le rapide — pas d'appels base de données, pas de calcul lourd
return NextResponse.next();
}
export const config = {
// Exécuter uniquement sur des chemins spécifiques
matcher: [
"/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
],
};Routes API à l'edge#
N'importe quel route handler peut opter pour l'edge runtime :
// app/api/hello/route.ts
export const runtime = "edge"; // Cette seule ligne change le runtime
export async function GET(request: Request) {
return Response.json({
message: "Bonjour depuis l'edge",
region: process.env.VERCEL_REGION ?? "unknown",
timestamp: Date.now(),
});
}Routes de page à l'edge#
Même des pages entières peuvent être rendues à l'edge, bien que j'y réfléchirais à deux fois avant de le faire :
// app/dashboard/page.tsx
export const runtime = "edge";
export default async function DashboardPage() {
// Rappel : pas d'APIs Node.js ici
// Toute récupération de données doit utiliser fetch() ou des clients compatibles edge
const data = await fetch("https://api.example.com/dashboard", {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
next: { revalidate: 60 },
}).then((r) => r.json());
return (
<main>
<h1>Tableau de bord</h1>
{/* afficher les données */}
</main>
);
}Ce qui est disponible dans l'edge runtime#
Voici une référence pratique de ce que vous pouvez et ne pouvez pas utiliser :
// ✅ Disponible à l'edge
fetch() // Requêtes HTTP
Request / Response // Requête/réponse standard Web
Headers // En-têtes HTTP
URL / URLSearchParams // Analyse d'URL
TextEncoder / TextDecoder // Encodage de chaînes
crypto.subtle // Opérations cryptographiques (signature, hachage)
crypto.randomUUID() // Génération d'UUID
crypto.getRandomValues() // Nombres aléatoires cryptographiques
structuredClone() // Clonage profond
atob() / btoa() // Encodage/décodage Base64
setTimeout() / setInterval() // Minuteries (mais souvenez-vous des limites CPU)
console.log() // Journalisation
ReadableStream / WritableStream // Streaming
AbortController / AbortSignal // Annulation de requête
URLPattern // Correspondance de pattern d'URL
// ❌ NON disponible à l'edge
require() // CommonJS (utilisez import)
fs / path / os // Modules intégrés de Node.js
process.exit() // Contrôle de processus
Buffer // Utilisez Uint8Array à la place
__dirname / __filename // Utilisez import.meta.url
setImmediate() // Pas un standard WebAuth à l'edge : le pattern complet#
Je veux approfondir l'authentification parce que c'est l'un des cas d'utilisation edge les plus impactants, mais c'est aussi facile de se tromper.
Le pattern qui fonctionne est : vérifier le jeton à l'edge, transmettre les claims de confiance en aval, ne jamais toucher la base de données dans le middleware.
// lib/edge-auth.ts — Utilitaires d'auth compatibles edge
import { jwtVerify, SignJWT, importSPKI, importPKCS8 } from "jose";
const PUBLIC_KEY_PEM = process.env.JWT_PUBLIC_KEY!;
const ISSUER = "https://auth.myapp.com";
const AUDIENCE = "https://myapp.com";
export interface TokenPayload {
sub: string;
email: string;
role: "user" | "admin" | "moderator";
iat: number;
exp: number;
}
export async function verifyToken(token: string): Promise<TokenPayload | null> {
try {
const publicKey = await importSPKI(PUBLIC_KEY_PEM, "RS256");
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ["RS256"],
issuer: ISSUER,
audience: AUDIENCE,
clockTolerance: 30, // 30 secondes de tolérance de décalage d'horloge
});
return payload as unknown as TokenPayload;
} catch {
return null;
}
}
export function isTokenExpiringSoon(payload: TokenPayload): boolean {
const now = Math.floor(Date.now() / 1000);
const fiveMinutes = 5 * 60;
return payload.exp - now < fiveMinutes;
}// middleware.ts — Le middleware d'authentification
import { NextRequest, NextResponse } from "next/server";
import { verifyToken, isTokenExpiringSoon } from "./lib/edge-auth";
const PUBLIC_PATHS = ["/", "/login", "/register", "/api/auth/login"];
const ADMIN_PATHS = ["/admin"];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Ignorer l'auth pour les chemins publics
if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith("/api/public"))) {
return NextResponse.next();
}
// Extraire le jeton
const token = request.cookies.get("auth-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Vérifier le jeton (pur crypto — aucun appel base de données)
const payload = await verifyToken(token);
if (!payload) {
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("auth-token");
return response;
}
// Contrôle d'accès basé sur les rôles
if (ADMIN_PATHS.some((p) => pathname.startsWith(p)) && payload.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
// Transmettre les infos utilisateur vérifiées à l'origine via des en-têtes de confiance
const response = NextResponse.next();
response.headers.set("x-user-id", payload.sub);
response.headers.set("x-user-email", payload.email);
response.headers.set("x-user-role", payload.role);
// Signaler si le jeton a besoin d'être rafraîchi
if (isTokenExpiringSoon(payload)) {
response.headers.set("x-token-refresh", "true");
}
return response;
}// app/api/profile/route.ts — Le serveur d'origine lit les en-têtes de confiance
export async function GET(request: Request) {
// Ces en-têtes ont été définis par le middleware edge après vérification JWT
// Ils sont de confiance parce qu'ils viennent de notre propre infrastructure
const userId = request.headers.get("x-user-id");
const userRole = request.headers.get("x-user-role");
if (!userId) {
return Response.json({ error: "Non autorisé" }, { status: 401 });
}
// Maintenant on peut interroger la base de données — on est sur le serveur d'origine,
// juste à côté de la base de données, avec un pool de connexions
const user = await db.user.findUnique({ where: { id: userId } });
return Response.json(user);
}L'idée clé : l'edge fait la partie rapide (vérification crypto), et l'origine fait la partie lente (requêtes base de données). Chacun s'exécute là où il est le plus efficace.
Une mise en garde importante : cela ne fonctionne que pour les JWTs. Si votre système d'auth nécessite une recherche en base de données à chaque requête (comme l'auth basée sur les sessions avec un cookie d'ID de session), l'edge ne peut pas aider — vous devriez quand même appeler la base de données, ce qui signifie un aller-retour vers la région d'origine.
Cache à l'edge#
Le cache à l'edge est là que les choses deviennent intéressantes. Les nœuds edge peuvent mettre en cache les réponses, ce qui signifie que les requêtes suivantes à la même URL sont servies directement depuis l'edge sans toucher votre origine.
Cache-Control bien fait#
// app/api/products/route.ts
export const runtime = "edge";
export async function GET(request: Request) {
const url = new URL(request.url);
const category = url.searchParams.get("category") ?? "all";
const products = await fetch(
`${process.env.ORIGIN_API}/products?category=${category}`,
).then((r) => r.json());
return Response.json(products, {
headers: {
// Cache sur le CDN pendant 60 secondes
// Servir du contenu périmé pendant la revalidation jusqu'à 5 minutes
// Le client peut cacher pendant 10 secondes
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300, max-age=10",
// Varier par ces en-têtes pour que différentes variantes obtiennent différentes entrées de cache
Vary: "Accept-Language, Accept-Encoding",
// Tag de cache spécifique au CDN pour une invalidation ciblée
"Cache-Tag": `products,category-${category}`,
},
});
}Le pattern stale-while-revalidate est particulièrement puissant à l'edge. Voici ce qui se passe :
- Première requête : l'edge récupère depuis l'origine, met en cache la réponse, la renvoie
- Requêtes dans les 60 secondes : l'edge sert depuis le cache (0 ms de latence d'origine)
- Requête entre 61 et 360 secondes : l'edge sert la version en cache périmée immédiatement, mais récupère une version fraîche depuis l'origine en arrière-plan
- Après 360 secondes : le cache est complètement expiré, la requête suivante va à l'origine
Vos utilisateurs obtiennent presque toujours une réponse en cache. Le compromis de fraîcheur est explicite et ajustable.
Edge Config pour la configuration dynamique#
Vercel Edge Config (et les services similaires d'autres plateformes) vous permet de stocker une configuration clé-valeur répliquée sur chaque emplacement edge. C'est incroyablement utile pour les feature flags, les règles de redirection et la configuration de tests A/B que vous voulez mettre à jour sans redéployer :
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
// Les lectures Edge Config sont extrêmement rapides (~1 ms) parce que
// les données sont répliquées sur chaque emplacement edge
const maintenanceMode = await get<boolean>("maintenance_mode");
if (maintenanceMode) {
return NextResponse.rewrite(new URL("/maintenance", request.url));
}
// Feature flags
const features = await get<Record<string, boolean>>("feature_flags");
if (features?.["new_pricing_page"] && request.nextUrl.pathname === "/pricing") {
return NextResponse.rewrite(new URL("/pricing-v2", request.url));
}
// Redirections dynamiques (mettre à jour les redirections sans redéploiement)
const redirects = await get<Array<{ from: string; to: string; permanent: boolean }>>(
"redirects",
);
if (redirects) {
const match = redirects.find((r) => r.from === request.nextUrl.pathname);
if (match) {
return NextResponse.redirect(
new URL(match.to, request.url),
match.permanent ? 308 : 307,
);
}
}
return NextResponse.next();
}C'est un véritable changement de paradigme. Avant Edge Config, changer un feature flag signifiait un changement de code et un redéploiement. Maintenant vous mettez à jour une valeur JSON dans un tableau de bord et elle se propage mondialement en quelques secondes.
Le vrai calcul de performance#
Faisons les vrais calculs au lieu du calcul marketing. Je vais comparer trois architectures pour un endpoint API typique qui doit interroger une base de données :
Scénario : API de profil utilisateur (2 requêtes base de données)#
Architecture A : Node.js régional traditionnel
Utilisateur à Tokyo → Origine en Virginie : 140 ms
+ Requête BDD 1 (même région) : 2 ms
+ Requête BDD 2 (même région) : 2 ms
+ Traitement : 5 ms
= Total : ~149 ms
Architecture B : Edge function avec base de données HTTP
Utilisateur à Tokyo → Edge à Tokyo : 5 ms
+ Requête BDD 1 (HTTP vers Virginie) : 145 ms
+ Requête BDD 2 (HTTP vers Virginie) : 145 ms
+ Traitement : 3 ms
= Total : ~298 ms ← PLUS LENT que le régional
Architecture C : Edge function avec base de données régionale (réplica en lecture)
Utilisateur à Tokyo → Edge à Tokyo : 5 ms
+ Requête BDD 1 (HTTP vers réplica Tokyo) : 8 ms
+ Requête BDD 2 (HTTP vers réplica Tokyo) : 8 ms
+ Traitement : 3 ms
= Total : ~24 ms ← Le plus rapide, mais nécessite une BDD multi-région
Architecture D : Edge pour l'auth + régional pour les données
Utilisateur à Tokyo → Edge middleware à Tokyo : 5 ms (vérification JWT)
→ Origine en Virginie : 140 ms
+ Requête BDD 1 (même région) : 2 ms
+ Requête BDD 2 (même région) : 2 ms
+ Traitement : 5 ms
= Total : ~154 ms
(Mais l'auth est déjà vérifiée — l'origine n'a pas besoin de re-vérifier)
(Et les requêtes non autorisées sont bloquées à l'edge — n'atteignent jamais l'origine)
Les enseignements :
- Edge + base de données d'origine = souvent plus lent qu'utiliser simplement un serveur régional
- Edge + base de données multi-région = le plus rapide mais le plus cher et le plus complexe
- Edge pour le filtrage + régional pour les données = meilleur compromis pragmatique
- Edge pur (sans base de données) = imbattable pour les redirections et vérifications d'auth
L'architecture D est ce que j'utilise pour la plupart des projets. L'edge gère ce qu'il fait bien (décisions rapides, auth, routage), et le serveur régional Node.js gère ce qu'il fait bien (requêtes base de données, calcul lourd).
Quand l'edge gagne vraiment : les opérations sans base de données#
Le calcul s'inverse complètement quand il n'y a pas de base de données impliquée :
Redirection (edge) :
Utilisateur à Tokyo → Edge à Tokyo → réponse de redirection : ~5 ms
Redirection (régional) :
Utilisateur à Tokyo → Origine en Virginie → réponse de redirection : ~280 ms
Réponse API statique (edge + cache) :
Utilisateur à Tokyo → Edge à Tokyo → réponse en cache : ~5 ms
Réponse API statique (régional) :
Utilisateur à Tokyo → Origine en Virginie → réponse : ~280 ms
Blocage de bot (edge) :
Bot malveillant n'importe où → Edge (le plus proche) → réponse 403 : ~5 ms
(Le bot n'atteint jamais votre serveur d'origine)
Blocage de bot (régional) :
Bot malveillant n'importe où → Origine en Virginie → réponse 403 : ~280 ms
(Le bot a quand même consommé des ressources d'origine)
Pour les opérations qui n'ont pas besoin de base de données, l'edge est 20 à 50 fois plus rapide. Ce n'est pas du marketing — c'est de la physique.
Mon cadre de décision#
Après un an de travail avec les edge functions en production, voici l'organigramme que j'utilise pour chaque nouvel endpoint ou morceau de logique :
Étape 1 : A-t-il besoin d'APIs Node.js ?#
S'il importe fs, net, child_process, ou un module natif — Node.js régional. Pas de débat.
Étape 2 : A-t-il besoin de requêtes base de données ?#
Si oui, et que vous n'avez pas de réplicas en lecture près de vos utilisateurs — Node.js régional (dans la même région que votre base de données). Les allers-retours vers la base de données domineront.
Si oui, et que vous avez des réplicas en lecture distribués mondialement — l'edge peut fonctionner, en utilisant des clients de base de données HTTP.
Étape 3 : Est-ce une décision sur une requête (routage, auth, redirection) ?#
Si oui — edge. C'est le point fort. Vous prenez une décision rapide qui détermine ce qui arrive à la requête avant qu'elle n'atteigne l'origine.
Étape 4 : La réponse est-elle cacheable ?#
Si oui — edge avec des en-têtes Cache-Control appropriés. Même si la première requête va à votre origine, les requêtes suivantes sont servies depuis le cache edge.
Étape 5 : Est-ce intensif en CPU ?#
Si cela implique un calcul significatif (traitement d'image, génération de PDF, transformations de données volumineuses) — Node.js régional.
Étape 6 : Quelle est la sensibilité à la latence ?#
Si c'est un job en arrière-plan ou un webhook — Node.js régional. Personne ne l'attend. Si c'est une requête face à l'utilisateur où chaque ms compte — edge, s'il remplit les autres critères.
L'aide-mémoire#
// ✅ PARFAIT pour l'edge
// - Middleware (auth, redirections, réécritures, en-têtes)
// - Logique de géolocalisation
// - Attribution de test A/B
// - Détection de bots / règles WAF
// - Réponses API favorables au cache
// - Vérification de feature flags
// - Réponses preflight CORS
// - Transformations de données statiques (sans BDD)
// - Vérification de signature de webhook
// ❌ GARDER sur Node.js régional
// - Opérations CRUD sur base de données
// - Uploads / traitement de fichiers
// - Manipulation d'images
// - Génération de PDF
// - Envoi d'emails (utilisez une API HTTP, mais toujours régional)
// - Serveurs WebSocket
// - Jobs en arrière-plan / files d'attente
// - Tout ce qui utilise des packages npm natifs
// - Pages SSR avec requêtes base de données
// - Résolveurs GraphQL qui interrogent des bases de données
// 🤔 ÇA DÉPEND
// - Authentification (edge pour JWT, régional pour session-BDD)
// - Routes API (edge si pas de BDD, régional si BDD)
// - Pages rendues côté serveur (edge si données viennent du cache/fetch, régional si BDD)
// - Fonctionnalités temps réel (edge pour l'auth initiale, régional pour les connexions persistantes)Ce que j'exécute réellement à l'edge#
Pour ce site, voici la répartition :
Edge (middleware) :
- Détection de locale et redirection
- Filtrage de bots
- En-têtes de sécurité (CSP, HSTS, etc.)
- Journalisation des accès
- Limitation de débit (basique)
Node.js régional :
- Rendu du contenu blog (le traitement MDX nécessite des APIs Node.js via Velite)
- Routes API qui touchent Redis
- Génération d'images OG (nécessite plus de temps CPU)
- Génération du flux RSS
Statique (aucun runtime) :
- Pages d'outils (pré-rendues au moment du build)
- Pages d'articles de blog (pré-rendues au moment du build)
- Toutes les images et assets (servis par CDN)
Le meilleur runtime est souvent aucun runtime. Si vous pouvez pré-rendre quelque chose au moment du build et le servir comme un asset statique, ce sera toujours plus rapide que n'importe quelle edge function. L'edge est pour les choses qui ont véritablement besoin d'être dynamiques à chaque requête.
Le résumé honnête#
Les edge functions ne sont pas un remplacement des serveurs traditionnels. Elles sont un complément. C'est un outil supplémentaire dans votre boîte à outils d'architecture — incroyablement puissant pour les bons cas d'utilisation et activement nuisible pour les mauvais.
L'heuristique à laquelle je reviens sans cesse : si votre fonction a besoin d'atteindre une base de données dans une seule région, mettre la fonction à l'edge n'aide pas — ça nuit. Vous avez juste ajouté un saut. La fonction s'exécute plus vite, mais elle passe ensuite 100 ms+ à revenir vers la base de données. Résultat net : plus lent que de tout exécuter dans une seule région.
Mais pour les décisions qui peuvent être prises uniquement avec les informations contenues dans la requête elle-même — géolocalisation, cookies, en-têtes, JWTs — l'edge est imbattable. Ces réponses edge de 5 ms ne sont pas des benchmarks synthétiques. Elles sont réelles, et vos utilisateurs sentent la différence.
Ne déplacez pas tout vers l'edge. Ne gardez pas tout hors de l'edge. Placez chaque morceau de logique là où la physique le favorise.