Edge Functions: Vad de är, när du ska använda dem och när du inte ska
Edge runtime, V8 isolates, myten om cold starts, georouting, A/B-testning, auth vid kanten och varför jag flyttade tillbaka vissa saker till Node.js. En balanserad titt på edge computing.
Ordet "edge" slängs runt mycket. Vercel säger det. Cloudflare säger det. Deno säger det. Varje konferensföredrag om webbprestanda nämner oundvikligen "att köra vid kanten" som om det vore en magisk besvärjelse som gör din app snabb.
Jag köpte det. Jag flyttade middleware, API-rutter, till och med viss renderingslogik till edge runtime. Vissa av de flytten var briljanta. Andra flyttade jag tyst tillbaka till Node.js tre veckor senare efter att ha debuggat connection pool-fel klockan 2 på natten.
Det här inlägget är den balanserade versionen av den historien — vad edge faktiskt är, var det genuint lyser, var det absolut inte gör det, och hur jag bestämmer vilken runtime att använda för varje del av min applikation.
Vad är Edge?#
Låt oss börja med geografi. När någon besöker din webbplats reser deras förfrågan från deras enhet, genom deras ISP, över internet till din server, bearbetas, och svaret reser hela vägen tillbaka. Om din server är i us-east-1 (Virginia) och din användare är i Tokyo, täcker den rundturen ungefär 14 000 km. Med ljusets hastighet genom fiber är det ungefär 70ms bara för fysiken — en väg. Lägg till DNS-upplösning, TLS-handskakning och eventuell bearbetningstid, och du tittar lätt på 200-400ms innan din användare ser en enda byte.
"Edge" innebär att köra din kod på servrar distribuerade globalt — samma CDN-noder som alltid har serverat statiska tillgångar, men nu kan de också exekvera din logik. Istället för en origin-server i Virginia körs din kod på 300+ platser världen över. En användare i Tokyo träffar en server i Tokyo. En användare i Paris träffar en server i Paris.
Latensmateematiken är enkel och övertygande:
Traditionell (single origin):
Tokyo → Virginia: ~140ms rundtur (enbart fysik)
+ TLS-handskakning: ~140ms till (ytterligare en rundtur)
+ Bearbetning: 20-50ms
Totalt: ~300-330ms
Edge (lokal PoP):
Tokyo → Tokyo edge-nod: ~5ms rundtur
+ TLS-handskakning: ~5ms till
+ Bearbetning: 5-20ms
Totalt: ~15-30ms
Det är en 10-20x förbättring för det initiala svaret. Det är verkligt, det är mätbart, och för vissa operationer är det transformativt.
Men här är vad marknadsföringen glider över: edge är inte en fullständig servermiljö. Det är något fundamentalt annorlunda.
V8 Isolates vs Node.js#
Traditionell Node.js körs i en fullständig operativsystemsprocess. Den har åtkomst till filsystemet, den kan öppna TCP-anslutningar, den kan starta barnprocesser, den kan läsa miljövariabler som en ström, den kan i princip göra allt en Linux-process kan göra.
Edge-funktioner körs inte på Node.js. De körs på V8 isolates — samma JavaScript-motor som driver Chrome, men nedskuren till sin kärna. Tänk på en V8 isolate som en lättviktig sandlåda:
// Detta fungerar i Node.js men INTE vid edge
import fs from "fs";
import { createConnection } from "net";
import { execSync } from "child_process";
const file = fs.readFileSync("/etc/hosts"); // ❌ Inget filsystem
const conn = createConnection({ port: 5432 }); // ❌ Ingen rå TCP
const result = execSync("ls -la"); // ❌ Inga barnprocesser
process.env.DATABASE_URL; // ⚠️ Tillgänglig men statisk, satt vid deployDet du HAR vid edge är Web API-ytan — samma API:er som finns i en webbläsare:
// Dessa fungerar alla vid 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();
// Web Streams API
const stream = new ReadableStream({
start(controller) {
controller.enqueue("chunk 1");
controller.enqueue("chunk 2");
controller.close();
},
});
// Cache API
const cache = caches.default;
await cache.put(request, response.clone());Begränsningarna är verkliga och hårda:
- Minne: 128MB per isolate (Cloudflare Workers), 256MB på vissa plattformar
- CPU-tid: 10-50ms av faktisk CPU-tid (inte väggklocketid —
await fetch()räknas inte, menJSON.parse()på en 5MB payload gör det) - Inga nativa moduler: Allt som behöver en C++-bindning (bcrypt, sharp, canvas) är uteslutet
- Inga persistenta anslutningar: Du kan inte hålla en databasanslutning öppen mellan förfrågningar
- Bundlestorleksbegränsningar: Typiskt 1-5MB för hela worker-skriptet
Det här är inte Node.js på ett CDN. Det är en annan runtime med en annan mental modell.
Cold Starts: Myten och verkligheten#
Du har förmodligen hört att edge-funktioner har "noll cold starts." Det här är... mestadels sant, och jämförelsen är genuint dramatisk.
En traditionell containerbaserad serverlös funktion (AWS Lambda, Google Cloud Functions) fungerar så här:
- Förfrågan anländer
- Plattformen tillhandahåller en container (om ingen är tillgänglig)
- Containern startar OS:et
- Runtime initialiseras (Node.js, Python, etc.)
- Din kod laddas och initialiseras
- Förfrågan bearbetas
Steg 2-5 är cold start. För en Node.js Lambda är detta typiskt 200-500ms. För en Java Lambda kan det vara 2-5 sekunder. För en .NET Lambda, 500ms-1.5s.
V8 isolates fungerar annorlunda:
- Förfrågan anländer
- Plattformen skapar en ny V8 isolate (eller återanvänder en varm)
- Din kod laddas (den är redan kompilerad till bytekod vid deploy)
- Förfrågan bearbetas
Steg 2-3 tar under 5ms. Ofta under 1ms. Isolaten är inte en container — det finns inget OS att starta, ingen runtime att initiera. V8 skapar en ny isolate på mikrosekunder. Frasen "noll cold start" är marknadsföringsspråk, men verkligheten (sub-5ms uppstart) är tillräckligt nära noll för att det inte spelar roll för de flesta användningsfall.
Men här är när cold starts fortfarande biter dig vid edge:
Stora bundles. Om din edge-funktion drar in 2MB beroenden behöver den koden fortfarande laddas och parsas. Jag lärde mig detta på den hårda vägen när jag bundlade ett valideringsbibliotek och ett datumformateringsbibliotek i en edge-middleware. Cold start gick från 2ms till 40ms. Fortfarande snabbt, men inte "noll."
Sällsynta platser. Edge-leverantörer har hundratals PoPs, men inte alla PoPs håller din kod varm. Om du får en förfrågan per timme från Nairobi, återvinns den isolaten mellan förfrågningar. Nästa förfrågan betalar uppstartskostnaden igen.
Flera isolates per förfrågan. Om din edge-funktion anropar en annan edge-funktion (eller om middleware och en API-rutt båda är edge), kan du spinna upp flera isolates för en användarförfrågan.
Det praktiska rådet: håll dina edge-funktionsbundles små. Importera bara det du behöver. Tree-shaka aggressivt. Ju mindre bundlen, desto snabbare cold start, desto mer håller löftet om "noll cold start".
// ❌ Gör inte detta vid edge
import dayjs from "dayjs";
import * as yup from "yup";
import lodash from "lodash";
// ✅ Gör detta istället — använd inbyggda API:er
const date = new Date().toISOString();
const isValid = typeof input === "string" && input.length < 200;
const unique = [...new Set(items)];Perfekta användningsfall för Edge Functions#
Efter att ha experimenterat extensivt har jag hittat ett tydligt mönster: edge-funktioner utmärker sig när du behöver fatta ett snabbt beslut om en förfrågan innan den når din origin-server. De är grindvakter, routrar och transformerare — inte applikationsservrar.
1. Geolokaliseringsbaserade omdirigeringar#
Det här är det ultimata användningsfallet. Förfrågan träffar den närmaste edge-noden, som redan vet var användaren befinner sig. Inget API-anrop behövs, ingen IP-uppslagningsdatabas — plattformen tillhandahåller geodata:
// middleware.ts — körs vid edge på varje förfrågan
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";
// Omdirigera till landsspecifik butik
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));
}
}
// Lägg till geohuvuden för nedströmsanvändning
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;
}Detta körs på under 5ms, precis bredvid användaren. Alternativet — att skicka förfrågan hela vägen till din origin-server bara för att göra en IP-uppslagning och omdirigera tillbaka — skulle kosta 100-300ms för användare långt från din origin.
2. A/B-testning utan klientflimmer#
A/B-testning på klientsidan orsakar det fruktade "blinket av originalinnehåll" — användaren ser version A under en bråkdel av en sekund innan JavaScript byter in version B. Vid edge kan du tilldela varianten innan sidan ens börjar renderas:
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Kontrollera om användaren redan har en varianttilldelning
const existingVariant = request.cookies.get("ab-variant")?.value;
if (existingVariant) {
// Skriv om till rätt variantsida
const url = request.nextUrl.clone();
url.pathname = `/variants/${existingVariant}${url.pathname}`;
return NextResponse.rewrite(url);
}
// Tilldela en ny variant (50/50-delning)
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 dagar
httpOnly: true,
sameSite: "lax",
});
return response;
}Användaren ser aldrig ett flimmer eftersom omskrivningen sker på nätverksnivån. Webbläsaren vet inte ens att det var ett A/B-test — den får bara variantsidan direkt.
3. Verifiering av autentiseringstoken#
Om din autentisering använder JWT:er (och du inte gör databassessionsuppslagningar), är edge perfekt. JWT-verifiering är ren kryptografi — ingen databas behövs:
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",
});
// Skicka användarinfo nedströms som huvuden
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 {
// Token har gått ut eller är ogiltig
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("session-token");
return response;
}
}Detta mönster är kraftfullt: edge-middlewaren verifierar tokenet och skickar användarinformation till din origin som betrodda huvuden. Dina API-rutter behöver inte verifiera tokenet igen — de läser bara request.headers.get("x-user-id").
4. Botdetektering och hastighetsbegränsning#
Edge-funktioner kan blockera oönskad trafik innan den någonsin når din origin:
import { NextRequest, NextResponse } from "next/server";
// Enkel in-memory hastighetsbegränsare (per edge-plats)
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") ?? "";
// Blockera kända dåliga bottar
const badBots = ["AhrefsBot", "SemrushBot", "MJ12bot", "DotBot"];
if (badBots.some((bot) => ua.includes(bot))) {
return new NextResponse("Forbidden", { status: 403 });
}
// Enkel hastighetsbegränsning
const now = Date.now();
const windowMs = 60_000; // 1 minut
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 });
}
// Periodisk rensning för att förhindra minnesläcka
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();
}En varning: hastighetsbegränsningskartan ovan är per-isolate, per-plats. Om du har 300 edge-platser har var och en sin egen karta. För strikt hastighetsbegränsning behöver du ett distribuerat lager som Upstash Redis eller Cloudflare Durable Objects. Men för grov missbruksprevention fungerar per-platsbegränsningar förvånansvärt bra.
5. Omskrivning av förfrågningar och personaliseringshuvuden#
Edge-funktioner är utmärkta på att transformera förfrågningar innan de når din origin:
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const url = request.nextUrl;
// Enhetsbaserad innehållsförhandling
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 från cookie
const flags = request.cookies.get("feature-flags")?.value;
if (flags) {
response.headers.set("x-feature-flags", flags);
}
// Språkdetektering för 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;
}Där Edge misslyckas#
Det här är avsnittet marknadsföringssidorna hoppar över. Jag har stött på var och en av dessa väggar.
1. Databasanslutningar#
Det här är den stora. Traditionella databaser (PostgreSQL, MySQL) använder persistenta TCP-anslutningar. En Node.js-server öppnar en anslutningspool vid uppstart och återanvänder de anslutningarna mellan förfrågningar. Effektivt, beprövat, väl förstått.
Edge-funktioner kan inte göra detta. Varje isolate är efemär. Det finns ingen "uppstartsfas" där du öppnar anslutningar. Även om du kunde öppna en anslutning kan isolaten återvinnas efter en förfrågan, vilket slösar bort anslutningsuppsättningstiden.
// ❌ Detta mönster fungerar fundamentalt inte vid edge
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // Anslutningspool med 10
});
// Varje edge-anrop skulle:
// 1. Skapa en ny Pool (kan inte återanvändas mellan anrop tillförlitligt)
// 2. Öppna en TCP-anslutning till din databas (som är i us-east-1, inte vid edge)
// 3. Göra TLS-handskakning med databasen
// 4. Köra frågan
// 5. Kassera anslutningen när isolaten återvinns
// Även med connection pooling-tjänster som PgBouncer
// betalar du fortfarande nätverkslatensen från edge → origin-databasDatabasrundturproblemet är fundamentalt. Din databas är i en region. Din edge-funktion är i 300 regioner. Varje databasfråga från edge måste resa från edge-platsen till databasregionen och tillbaka. För en användare i Tokyo som träffar en Tokyo edge-nod, men din databas är i Virginia:
Edge-funktion i Tokyo
→ Fråga till PostgreSQL i Virginia: ~140ms rundtur
→ Andra frågan: ~140ms till
→ Totalt: 280ms bara för två frågor
Node.js-funktion i Virginia (samma region som DB)
→ Fråga till PostgreSQL: ~1ms rundtur
→ Andra frågan: ~1ms till
→ Totalt: 2ms för två frågor
Edge-funktionen är 140x långsammare för databasoperationer i detta scenario. Det spelar ingen roll att edge-funktionen startade snabbare — databasrundturerna dominerar allt.
Det är därför HTTP-baserade databasproxies finns (Neons serverlösa drivrutin, PlanetScales fetch-baserade drivrutin, Supabases REST API). De fungerar, men de gör fortfarande HTTP-förfrågningar till en databas i en enda region. De löser problemet "kan inte använda TCP" men inte problemet "databasen är långt borta".
// ✅ Detta fungerar vid edge (HTTP-baserad databasåtkomst)
// Men det är fortfarande långsamt om databasen är långt från edge-noden
import { neon } from "@neondatabase/serverless";
export const runtime = "edge";
export async function GET(request: Request) {
const sql = neon(process.env.DATABASE_URL!);
// Detta gör en HTTP-förfrågan till din Neon-databas
// Fungerar, men latens beror på avstånd till databasregionen
const posts = await sql`SELECT * FROM posts WHERE published = true LIMIT 10`;
return Response.json(posts);
}2. Långkörande uppgifter#
Edge-funktioner har CPU-tidsbegränsningar, typiskt 10-50ms av faktisk beräkningstid. Väggklocketid är generösare (vanligtvis 30 sekunder), men CPU-intensiva operationer träffar gränsen snabbt:
// ❌ Dessa kommer att överskrida CPU-tidsbegränsningar vid edge
export const runtime = "edge";
export async function POST(request: Request) {
const data = await request.json();
// Bildbearbetning — CPU-intensivt
// (Kan inte heller använda sharp eftersom det är en nativ modul)
const processed = heavyImageProcessing(data.image);
// PDF-generering — CPU-intensivt + behöver Node.js API:er
const pdf = generatePDF(data.content);
// Stor datatransformation
const result = data.items // 100 000 objekt
.map(transform)
.filter(validate)
.sort(compare)
.reduce(aggregate, {});
return Response.json(result);
}Om din funktion behöver mer än några millisekunders CPU-tid hör den hemma på en regional Node.js-server. Punkt.
3. Beroenden som bara fungerar i Node.js#
Den här fångar folk oförberett. Ett förvånansvärt antal npm-paket beror på Node.js inbyggda moduler:
// ❌ Dessa paket fungerar inte vid edge
import bcrypt from "bcrypt"; // Nativ C++-bindning
import sharp from "sharp"; // Nativ C++-bindning
import puppeteer from "puppeteer"; // Behöver filsystem + child_process
import nodemailer from "nodemailer"; // Behöver net-modul
import { readFile } from "fs/promises"; // Node.js filsystems-API
import mongoose from "mongoose"; // TCP-anslutningar + Node.js API:er
// ✅ Edge-kompatibla alternativ
import { hashSync } from "bcryptjs"; // Ren JS-implementation (långsammare)
// För bilder: använd en separat tjänst eller API
// För e-post: använd ett HTTP-baserat e-post-API (Resend, SendGrid REST)
// För databas: använd HTTP-baserade klienterInnan du flyttar något till edge, kontrollera varje beroende. En require("fs") begravd tre nivåer djupt i ditt beroendeträd kommer att krascha din edge-funktion vid körtid — inte vid byggtid. Du deployer, allt ser bra ut, sedan träffar den första förfrågan den kodsökvägen och du får ett kryptiskt felmeddelande.
4. Stora bundlestorlekar#
Edge-plattformar har strikta bundlestorleksbegränsningar:
- Cloudflare Workers: 1MB (gratis), 5MB (betald)
- Vercel Edge Functions: 4MB (komprimerad)
- Deno Deploy: 20MB
Det här låter som gott om utrymme tills du importerar ett UI-komponentbibliotek, ett valideringsbibliotek och ett datumbibliotek. Jag hade en gång en edge-middleware som ballongerade till 3.5MB för att jag importerade från en barrel-fil som drog in hela @/components-katalogen.
// ❌ Barrel-filimporter kan dra in alldeles för mycket
import { validateEmail } from "@/lib/utils";
// Om utils.ts re-exporterar från 20 andra moduler buntas alla ihop
// ✅ Importera direkt från källan
import { validateEmail } from "@/lib/validators/email";5. Streaming och WebSockets#
Edge-funktioner kan göra strömmande svar (Web Streams API), men långlivade WebSocket-anslutningar är en annan historia. Även om vissa plattformar stöder WebSockets vid edge (Cloudflare Workers, Deno Deploy), gör edge-funktionernas efemära natur dem till ett dåligt val för tillståndsbaserade, långlivade anslutningar.
Next.js Edge Runtime#
Next.js gör det enkelt att välja edge runtime per rutt. Du behöver inte satsa allt — du väljer exakt vilka rutter som körs vid edge.
Middleware (alltid Edge)#
Next.js middleware körs alltid vid edge. Det är designat så — middleware fångar upp varje matchande förfrågan, så det behöver vara snabbt och globalt distribuerat:
// middleware.ts — körs alltid vid edge, inget opt-in behövs
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Detta körs före varje matchande förfrågan
// Håll det snabbt — inga databasanrop, inga tunga beräkningar
return NextResponse.next();
}
export const config = {
// Kör bara på specifika sökvägar
matcher: [
"/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
],
};API-rutter vid Edge#
Vilken rutthanterare som helst kan välja edge runtime:
// app/api/hello/route.ts
export const runtime = "edge"; // Denna enda rad ändrar runtime
export async function GET(request: Request) {
return Response.json({
message: "Hej från edge",
region: process.env.VERCEL_REGION ?? "unknown",
timestamp: Date.now(),
});
}Sidrutter vid Edge#
Till och med hela sidor kan renderas vid edge, men jag skulle tänka noga innan jag gör detta:
// app/dashboard/page.tsx
export const runtime = "edge";
export default async function DashboardPage() {
// Kom ihåg: inga Node.js API:er här
// All datahämtning måste använda fetch() eller edge-kompatibla klienter
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>Dashboard</h1>
{/* rendera data */}
</main>
);
}Vad som är tillgängligt i Edge Runtime#
Här är en praktisk referens för vad du kan och inte kan använda:
// ✅ Tillgängligt vid edge
fetch() // HTTP-förfrågningar
Request / Response // Webbstandard request/response
Headers // HTTP-huvuden
URL / URLSearchParams // URL-parsning
TextEncoder / TextDecoder // Strängkodning
crypto.subtle // Kryptooperationer (signering, hashning)
crypto.randomUUID() // UUID-generering
crypto.getRandomValues() // Kryptografiska slumptal
structuredClone() // Djup kloning
atob() / btoa() // Base64-kodning/avkodning
setTimeout() / setInterval() // Timers (men kom ihåg CPU-begränsningar)
console.log() // Loggning
ReadableStream / WritableStream // Streaming
AbortController / AbortSignal // Avbrytning av förfrågningar
URLPattern // URL-mönstermatchning
// ❌ INTE tillgängligt vid edge
require() // CommonJS (använd import)
fs / path / os // Node.js inbyggda moduler
process.exit() // Processkontroll
Buffer // Använd Uint8Array istället
__dirname / __filename // Använd import.meta.url
setImmediate() // Inte en webbstandardAuth vid Edge: Det fullständiga mönstret#
Jag vill gå djupare in på autentisering eftersom det är ett av de mest påverkande edge-användningsfallen, men det är också lätt att göra fel.
Mönstret som fungerar är: verifiera token vid edge, skicka betrodda claims nedströms, rör aldrig databasen i middleware.
// lib/edge-auth.ts — Edge-kompatibla autentiseringsverktyg
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 sekunders klockskevtolerans
});
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 — Auth-middlewaren
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;
// Hoppa över auth för publika sökvägar
if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith("/api/public"))) {
return NextResponse.next();
}
// Extrahera token
const token = request.cookies.get("auth-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Verifiera token (ren kryptografi — inget databasanrop)
const payload = await verifyToken(token);
if (!payload) {
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("auth-token");
return response;
}
// Rollbaserad åtkomstkontroll
if (ADMIN_PATHS.some((p) => pathname.startsWith(p)) && payload.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
// Skicka verifierad användarinfo till origin som betrodda huvuden
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);
// Signalera om token behöver uppdateras
if (isTokenExpiringSoon(payload)) {
response.headers.set("x-token-refresh", "true");
}
return response;
}// app/api/profile/route.ts — Origin-servern läser betrodda huvuden
export async function GET(request: Request) {
// Dessa huvuden sattes av edge-middleware efter JWT-verifiering
// De är betrodda eftersom de kommer från vår egen infrastruktur
const userId = request.headers.get("x-user-id");
const userRole = request.headers.get("x-user-role");
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// Nu kan vi anropa databasen — vi är på origin-servern,
// precis bredvid databasen, med en anslutningspool
const user = await db.user.findUnique({ where: { id: userId } });
return Response.json(user);
}Den viktiga insikten: edge gör den snabba delen (kryptoverifiering), och origin gör den långsamma delen (databasfrågor). Var och en körs där den är mest effektiv.
En viktig varning: detta fungerar bara för JWT:er. Om ditt autentiseringssystem kräver en databasuppslagning vid varje förfrågan (som sessionsbaserad auth med en sessions-ID-cookie), kan edge inte hjälpa — du skulle fortfarande behöva anropa databasen, vilket innebär en rundtur till origin-regionen.
Edge-cachning#
Cachning vid edge är där saker blir intressanta. Edge-noder kan cacha svar, vilket innebär att efterföljande förfrågningar till samma URL serveras direkt från edge utan att träffa din origin alls.
Cache-Control gjort rätt#
// 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: {
// Cacha på CDN i 60 sekunder
// Servera gammalt medan revalidering i upp till 5 minuter
// Klient kan cacha i 10 sekunder
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300, max-age=10",
// Variera efter dessa huvuden så att olika varianter får olika cacheposter
Vary: "Accept-Language, Accept-Encoding",
// CDN-specifik cachetagg för riktad invalidering
"Cache-Tag": `products,category-${category}`,
},
});
}Mönstret stale-while-revalidate är särskilt kraftfullt vid edge. Här är vad som händer:
- Första förfrågan: Edge hämtar från origin, cachar svaret, returnerar det
- Förfrågningar inom 60 sekunder: Edge serverar från cache (0ms origin-latens)
- Förfrågan vid 61-360 sekunder: Edge serverar den gamla cachade versionen omedelbart, men hämtar en fräsch version från origin i bakgrunden
- Efter 360 sekunder: Cachen är fullt utgången, nästa förfrågan går till origin
Dina användare får nästan alltid ett cachat svar. Fräschhetskompromissen är explicit och justerbar.
Edge Config för dynamisk konfiguration#
Vercels Edge Config (och liknande tjänster från andra plattformar) låter dig lagra nyckel-värde-konfiguration som replikeras till varje edge-plats. Det här är otroligt användbart för feature flags, omdirigeringsregler och A/B-testkonfiguration som du vill uppdatera utan att omdeployea:
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
// Edge Config-läsningar är extremt snabba (~1ms) eftersom
// datan replikeras till varje edge-plats
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));
}
// Dynamiska omdirigeringar (uppdatera omdirigeringar utan omdeploy)
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();
}Det här är en genuint spelförändrande funktion. Före Edge Config innebar att ändra en feature flag en kodändring och omdeploy. Nu uppdaterar du ett JSON-värde i en dashboard och det sprids globalt på sekunder.
Den verkliga prestandamatematiken#
Låt oss göra den ärliga matematiken istället för marknadsföringsmatematiken. Jag jämför tre arkitekturer för en typisk API-endpoint som behöver fråga en databas:
Scenario: Användarprofil-API (2 databasfrågor)#
Arkitektur A: Traditionell regional Node.js
Användare i Tokyo → Origin i Virginia: 140ms
+ DB-fråga 1 (samma region): 2ms
+ DB-fråga 2 (samma region): 2ms
+ Bearbetning: 5ms
= Totalt: ~149ms
Arkitektur B: Edge-funktion med HTTP-databas
Användare i Tokyo → Edge i Tokyo: 5ms
+ DB-fråga 1 (HTTP till Virginia): 145ms
+ DB-fråga 2 (HTTP till Virginia): 145ms
+ Bearbetning: 3ms
= Totalt: ~298ms ← LÅNGSAMMARE än regional
Arkitektur C: Edge-funktion med regional databas (läsreplika)
Användare i Tokyo → Edge i Tokyo: 5ms
+ DB-fråga 1 (HTTP till Tokyo-replika): 8ms
+ DB-fråga 2 (HTTP till Tokyo-replika): 8ms
+ Bearbetning: 3ms
= Totalt: ~24ms ← Snabbast, men kräver multiregion-DB
Arkitektur D: Edge för auth + regional för data
Användare i Tokyo → Edge-middleware i Tokyo: 5ms (JWT-verifiering)
→ Origin i Virginia: 140ms
+ DB-fråga 1 (samma region): 2ms
+ DB-fråga 2 (samma region): 2ms
+ Bearbetning: 5ms
= Totalt: ~154ms
(Men auth är redan verifierad — origin behöver inte verifiera igen)
(Och obehöriga förfrågningar blockeras vid edge — når aldrig origin)
Slutsatserna:
- Edge + origin-databas = ofta långsammare än att bara använda en regional server
- Edge + multiregion-databas = snabbast men dyrast och mest komplext
- Edge för grindvakt + regional för data = bästa pragmatiska balansen
- Ren edge (ingen databas) = oslagbart för saker som omdirigeringar och auth-kontroller
Arkitektur D är vad jag använder för de flesta projekt. Edge hanterar det den är bra på (snabba beslut, auth, routing), och den regionala Node.js-servern hanterar det den är bra på (databasfrågor, tung beräkning).
När Edge verkligen vinner: Operationer utan databas#
Matematiken vänder helt när det inte finns någon databas inblandad:
Omdirigering (edge):
Användare i Tokyo → Edge i Tokyo → omdirigeringssvar: ~5ms
Omdirigering (regional):
Användare i Tokyo → Origin i Virginia → omdirigeringssvar: ~280ms
Statiskt API-svar (edge + cache):
Användare i Tokyo → Edge i Tokyo → cachat svar: ~5ms
Statiskt API-svar (regional):
Användare i Tokyo → Origin i Virginia → svar: ~280ms
Botblockering (edge):
Dålig bot var som helst → Edge (närmaste) → 403-svar: ~5ms
(Boten når aldrig din origin-server)
Botblockering (regional):
Dålig bot var som helst → Origin i Virginia → 403-svar: ~280ms
(Boten förbrukade fortfarande origin-resurser)
För operationer som inte behöver en databas är edge 20-50x snabbare. Det här är inte marknadsföring — det är fysik.
Mitt beslutsramverk#
Efter ett år av att arbeta med edge-funktioner i produktion, här är flödesschemat jag använder för varje ny endpoint eller logikdel:
Steg 1: Behöver det Node.js API:er?#
Om det importerar fs, net, child_process eller någon nativ modul — Node.js regional. Ingen diskussion.
Steg 2: Behöver det databasfrågor?#
Om ja, och du inte har läsreplikor nära dina användare — Node.js regional (i samma region som din databas). Databasrundturerna kommer att dominera.
Om ja, och du har globalt distribuerade läsreplikor — Edge kan fungera, med HTTP-baserade databasklienter.
Steg 3: Är det ett beslut om en förfrågan (routing, auth, omdirigering)?#
Om ja — Edge. Det här är sweet spot. Du fattar ett snabbt beslut som avgör vad som händer med förfrågan innan den når origin.
Steg 4: Är svaret cachbart?#
Om ja — Edge med korrekta Cache-Control-huvuden. Även om den första förfrågan går till din origin serveras efterföljande förfrågningar från edge-cachen.
Steg 5: Är det CPU-intensivt?#
Om det involverar betydande beräkning (bildbearbetning, PDF-generering, stora datatransformeringar) — Node.js regional.
Steg 6: Hur latenskänsligt är det?#
Om det är ett bakgrundsjobb eller webhook — Node.js regional. Ingen väntar på det. Om det är en användarvänd förfrågan där varje ms spelar roll — Edge, om det uppfyller de andra kriterierna.
Fusklappen#
// ✅ PERFEKT för edge
// - Middleware (auth, omdirigeringar, omskrivningar, huvuden)
// - Geolokaliseringslogik
// - A/B-testtilldelning
// - Botdetektering / WAF-regler
// - Cachevänliga API-svar
// - Feature flag-kontroller
// - CORS preflight-svar
// - Statiska datatransformeringar (ingen DB)
// - Webhook-signaturverifiering
// ❌ BEHÅLL på Node.js regional
// - Databas-CRUD-operationer
// - Filuppladdningar / bearbetning
// - Bildmanipulation
// - PDF-generering
// - E-postutskick (använd HTTP API, men fortfarande regional)
// - WebSocket-servrar
// - Bakgrundsjobb / köer
// - Allt som använder nativa npm-paket
// - SSR-sidor med databasfrågor
// - GraphQL-resolvers som anropar databaser
// 🤔 DET BEROR PÅ
// - Autentisering (edge för JWT, regional för session-DB)
// - API-rutter (edge om ingen DB, regional om DB)
// - Serverrenderade sidor (edge om data kommer från cache/fetch, regional om DB)
// - Realtidsfunktioner (edge för initial auth, regional för persistenta anslutningar)Vad jag faktiskt kör vid Edge#
För den här sajten, här är uppdelningen:
Edge (middleware):
- Språkdetektering och omdirigering
- Botfiltrering
- Säkerhetshuvuden (CSP, HSTS, etc.)
- Åtkomstloggning
- Hastighetsbegränsning (grundläggande)
Node.js regional:
- Blogginnehållsrendering (MDX-bearbetning behöver Node.js API:er genom Velite)
- API-rutter som berör Redis
- OG-bildgenerering (behöver mer CPU-tid)
- RSS-flödesgenerering
Statiskt (ingen runtime alls):
- Verktygssidor (förrenderade vid byggtid)
- Blogginläggssidor (förrenderade vid byggtid)
- Alla bilder och tillgångar (CDN-serverade)
Den bästa runtime är ofta ingen runtime. Om du kan förrendera något vid byggtid och servera det som en statisk tillgång kommer det alltid att vara snabbare än någon edge-funktion. Edge är för de saker som genuint behöver vara dynamiska vid varje förfrågan.
Den ärliga sammanfattningen#
Edge-funktioner är inte en ersättning för traditionella servrar. De är ett komplement. De är ett ytterligare verktyg i din arkitekturverktygslåda — ett som är otroligt kraftfullt för rätt användningsfall och aktivt skadligt för fel.
Heuristiken jag ständigt återkommer till: om din funktion behöver nå ut till en databas i en enda region, hjälper det inte att placera funktionen vid edge — det skadar. Du har bara lagt till ett hopp. Funktionen körs snabbare, men sedan spenderar den 100ms+ på att nå tillbaka till databasen. Nettoresultat: långsammare än att köra allt i en region.
Men för beslut som kan fattas med enbart informationen i själva förfrågan — geolokalisering, cookies, huvuden, JWT:er — är edge oslagbart. De där 5ms edge-svaren är inte syntetiska benchmarks. De är verkliga, och dina användare känner skillnaden.
Flytta inte allt till edge. Håll inte allt borta från edge. Placera varje logikdel där fysiken gynnar det.