Funkcje Edge: Czym są, kiedy ich używać i kiedy nie
Środowisko uruchomieniowe edge, izolaty V8, mit zimnego startu, geo-routing, testy A/B, uwierzytelnianie na edge i dlaczego niektóre rzeczy przeniosłem z powrotem na Node.js. Zbalansowane spojrzenie na edge computing.
Słowo „edge" jest używane na prawo i lewo. Vercel to mówi. Cloudflare to mówi. Deno to mówi. Każda prezentacja konferencyjna o wydajności stron internetowych nieuchronnie wspomina o „uruchamianiu na edge" jakby to było magiczne zaklęcie, które sprawia, że Twoja aplikacja jest szybka.
Uwierzyłem w to. Przeniosłem middleware, trasy API, a nawet część logiki renderowania do środowiska uruchomieniowego edge. Niektóre z tych ruchów były genialne. Inne po cichu przeniosłem z powrotem na Node.js trzy tygodnie później, po debugowaniu błędów puli połączeń o 2 w nocy.
Ten post to zbalansowana wersja tej historii — czym naprawdę jest edge, gdzie faktycznie się sprawdza, gdzie absolutnie nie i jak decyduję, którego środowiska uruchomieniowego użyć dla każdego elementu mojej aplikacji.
Czym jest Edge?#
Zacznijmy od geografii. Kiedy ktoś odwiedza Twoją stronę, żądanie podróżuje z jego urządzenia, przez jego dostawcę internetu, przez internet do Twojego serwera, jest przetwarzane, a odpowiedź wraca tą samą drogą. Jeśli Twój serwer jest w us-east-1 (Virginia), a Twój użytkownik jest w Tokio, ta podróż tam i z powrotem obejmuje około 14 000 km. Przy prędkości światła w światłowodzie to około 70ms tylko za fizykę — w jedną stronę. Dodaj rozwiązywanie DNS, handshake TLS i czas przetwarzania, a łatwo uzyskasz 200-400ms zanim Twój użytkownik zobaczy choćby jeden bajt.
„Edge" oznacza uruchamianie Twojego kodu na serwerach rozproszonych globalnie — tych samych węzłach CDN, które zawsze serwowały zasoby statyczne, ale teraz mogą również wykonywać Twoją logikę. Zamiast jednego serwera źródłowego w Virginii, Twój kod działa w ponad 300 lokalizacjach na świecie. Użytkownik w Tokio trafia na serwer w Tokio. Użytkownik w Paryżu trafia na serwer w Paryżu.
Matematyka opóźnień jest prosta i przekonująca:
Tradycyjne (jeden serwer źródłowy):
Tokio → Virginia: ~140ms podróż w obie strony (sama fizyka)
+ Handshake TLS: ~140ms więcej (kolejna podróż)
+ Przetwarzanie: 20-50ms
Razem: ~300-330ms
Edge (lokalny PoP):
Tokio → węzeł edge w Tokio: ~5ms podróż w obie strony
+ Handshake TLS: ~5ms więcej
+ Przetwarzanie: 5-20ms
Razem: ~15-30ms
To 10-20-krotna poprawa dla początkowej odpowiedzi. To jest realne, mierzalne i dla pewnych operacji transformacyjne.
Ale oto co marketing pomija: edge nie jest pełnym środowiskiem serwerowym. To coś fundamentalnie innego.
Izolaty V8 vs Node.js#
Tradycyjny Node.js działa w pełnym procesie systemu operacyjnego. Ma dostęp do systemu plików, może otwierać połączenia TCP, może uruchamiać procesy potomne, może odczytywać zmienne środowiskowe jako strumień, może robić zasadniczo wszystko, co może zrobić proces Linux.
Funkcje edge nie działają na Node.js. Działają na izolatach V8 — tym samym silniku JavaScript, który napędza Chrome, ale okrojonym do rdzenia. Pomyśl o izolacie V8 jak o lekkim sandboxie:
// To działa w Node.js, ale NIE na edge
import fs from "fs";
import { createConnection } from "net";
import { execSync } from "child_process";
const file = fs.readFileSync("/etc/hosts"); // ❌ Brak systemu plików
const conn = createConnection({ port: 5432 }); // ❌ Brak surowego TCP
const result = execSync("ls -la"); // ❌ Brak procesów potomnych
process.env.DATABASE_URL; // ⚠️ Dostępne, ale statyczne, ustawiane w czasie wdrożeniaTo, co MASZ na edge, to powierzchnia Web API — te same API dostępne w przeglądarce:
// To wszystko działa na 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());Ograniczenia są realne i twarde:
- Pamięć: 128MB na izolat (Cloudflare Workers), 256MB na niektórych platformach
- Czas CPU: 10-50ms rzeczywistego czasu CPU (nie czasu zegara —
await fetch()się nie liczy, aleJSON.parse()na 5MB payloadzie już tak) - Brak natywnych modułów: Wszystko, co wymaga bindingu C++ (bcrypt, sharp, canvas) odpada
- Brak trwałych połączeń: Nie możesz utrzymywać otwartego połączenia z bazą danych między żądaniami
- Limity rozmiaru bundla: Zazwyczaj 1-5MB na cały skrypt workera
To nie jest Node.js na CDN. To inne środowisko uruchomieniowe z innym modelem mentalnym.
Zimne starty: Mit i rzeczywistość#
Prawdopodobnie słyszałeś, że funkcje edge mają „zero zimnych startów". To jest... w większości prawda, a porównanie jest naprawdę dramatyczne.
Tradycyjna funkcja serverless oparta na kontenerach (AWS Lambda, Google Cloud Functions) działa tak:
- Żądanie przychodzi
- Platforma provisionuje kontener (jeśli żaden nie jest dostępny)
- Kontener uruchamia system operacyjny
- Środowisko uruchomieniowe się inicjalizuje (Node.js, Python, itd.)
- Twój kod się ładuje i inicjalizuje
- Żądanie jest przetwarzane
Kroki 2-5 to zimny start. Dla Lambda Node.js to zazwyczaj 200-500ms. Dla Lambda Java to może być 2-5 sekund. Dla Lambda .NET, 500ms-1.5s.
Izolaty V8 działają inaczej:
- Żądanie przychodzi
- Platforma tworzy nowy izolat V8 (lub ponownie używa ciepłego)
- Twój kod się ładuje (jest już skompilowany do bytecodu w czasie wdrożenia)
- Żądanie jest przetwarzane
Kroki 2-3 zajmują poniżej 5ms. Często poniżej 1ms. Izolat to nie kontener — nie ma systemu operacyjnego do uruchomienia, nie ma środowiska uruchomieniowego do zainicjalizowania. V8 tworzy świeży izolat w mikrosekundach. Fraza „zero zimnego startu" to marketingowy slogan, ale rzeczywistość (sub-5ms startup) jest wystarczająco bliska zeru, że dla większości przypadków użycia nie ma znaczenia.
Ale oto kiedy zimne starty na edge wciąż dają się we znaki:
Duże bundle. Jeśli Twoja funkcja edge ściąga 2MB zależności, ten kod wciąż musi zostać załadowany i sparsowany. Nauczyłem się tego na własnej skórze, kiedy zbundlowałem bibliotekę walidacji i bibliotekę formatowania dat do middleware edge. Zimny start wzrósł z 2ms do 40ms. Wciąż szybko, ale nie „zero".
Rzadkie lokalizacje. Dostawcy edge mają setki PoP-ów, ale nie wszystkie PoP-y utrzymują Twój kod w stanie ciepłym. Jeśli dostajesz jedno żądanie na godzinę z Nairobi, ten izolat jest recyclowany między żądaniami. Następne żądanie płaci koszt startu od nowa.
Wiele izolatów na żądanie. Jeśli Twoja funkcja edge wywołuje inną funkcję edge (lub jeśli zarówno middleware, jak i trasa API są edge), możesz uruchamiać wiele izolatów dla jednego żądania użytkownika.
Praktyczna rada: utrzymuj bundle swoich funkcji edge małe. Importuj tylko to, co potrzebujesz. Agresywnie tree-shake'uj. Im mniejszy bundle, tym szybszy zimny start, tym bardziej obietnica „zero zimnego startu" się sprawdza.
// ❌ Nie rób tego na edge
import dayjs from "dayjs";
import * as yup from "yup";
import lodash from "lodash";
// ✅ Rób to zamiast tego — używaj wbudowanych API
const date = new Date().toISOString();
const isValid = typeof input === "string" && input.length < 200;
const unique = [...new Set(items)];Idealne przypadki użycia funkcji Edge#
Po intensywnym eksperymentowaniu znalazłem wyraźny wzorzec: funkcje edge wyróżniają się, kiedy musisz podjąć szybką decyzję o żądaniu zanim dotrze ono do Twojego serwera źródłowego. Są strażnikami, routerami i transformerami — nie serwerami aplikacyjnymi.
1. Przekierowania oparte na geolokalizacji#
To jest zabójczy przypadek użycia. Żądanie trafia do najbliższego węzła edge, który już wie, gdzie jest użytkownik. Nie potrzeba żadnego wywołania API, żadnej bazy danych IP lookup — platforma dostarcza dane geo:
// middleware.ts — działa na edge przy każdym żądaniu
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";
// Przekierowanie do sklepu specyficznego dla kraju
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));
}
}
// Dodaj nagłówki geo do dalszego użycia
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;
}To działa w mniej niż 5ms, tuż obok użytkownika. Alternatywa — wysłanie żądania aż do serwera źródłowego tylko po to, by zrobić IP lookup i przekierować z powrotem — kosztowałoby 100-300ms dla użytkowników daleko od Twojego serwera.
2. Testy A/B bez migotania po stronie klienta#
Testowanie A/B po stronie klienta powoduje przerażające „mignięcie oryginalnej treści" — użytkownik widzi wersję A na ułamek sekundy zanim JavaScript podmieni na wersję B. Na edge możesz przypisać wariant zanim strona w ogóle zacznie się renderować:
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Sprawdź czy użytkownik już ma przypisany wariant
const existingVariant = request.cookies.get("ab-variant")?.value;
if (existingVariant) {
// Przepisz na poprawną stronę wariantu
const url = request.nextUrl.clone();
url.pathname = `/variants/${existingVariant}${url.pathname}`;
return NextResponse.rewrite(url);
}
// Przypisz nowy wariant (podział 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 dni
httpOnly: true,
sameSite: "lax",
});
return response;
}Użytkownik nigdy nie widzi migotania, ponieważ przepisanie dzieje się na poziomie sieci. Przeglądarka nawet nie wie, że to był test A/B — po prostu otrzymuje stronę wariantu bezpośrednio.
3. Weryfikacja tokenów uwierzytelniających#
Jeśli Twoje uwierzytelnianie używa JWT (i nie robisz lookupów sesji w bazie danych), edge jest idealny. Weryfikacja JWT to czysta kryptografia — nie potrzeba bazy danych:
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",
});
// Przekaż informacje o użytkowniku dalej jako nagłówki
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 wygasł lub jest nieprawidłowy
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("session-token");
return response;
}
}Ten wzorzec jest potężny: middleware edge weryfikuje token i przekazuje informacje o użytkowniku do serwera źródłowego jako zaufane nagłówki. Twoje trasy API nie muszą ponownie weryfikować tokenu — po prostu odczytują request.headers.get("x-user-id").
4. Wykrywanie botów i ograniczanie częstotliwości#
Funkcje edge mogą blokować niechciany ruch zanim w ogóle dotrze do Twojego serwera źródłowego:
import { NextRequest, NextResponse } from "next/server";
// Prosty limiter częstotliwości w pamięci (na lokalizację 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") ?? "";
// Blokuj znane złe boty
const badBots = ["AhrefsBot", "SemrushBot", "MJ12bot", "DotBot"];
if (badBots.some((bot) => ua.includes(bot))) {
return new NextResponse("Forbidden", { status: 403 });
}
// Proste ograniczanie częstotliwości
const now = Date.now();
const windowMs = 60_000; // 1 minuta
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 });
}
// Okresowe czyszczenie, aby zapobiec wyciekowi pamięci
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();
}Jedno zastrzeżenie: mapa limitu częstotliwości powyżej jest per-izolat, per-lokalizacja. Jeśli masz 300 lokalizacji edge, każda ma własną mapę. Dla ścisłego ograniczania częstotliwości potrzebujesz rozproszonego magazynu jak Upstash Redis lub Cloudflare Durable Objects. Ale do ogólnego zapobiegania nadużyciom, limity per-lokalizacja działają zaskakująco dobrze.
5. Przepisywanie żądań i nagłówki personalizacji#
Funkcje edge doskonale radzą sobie z transformacją żądań zanim dotrą do Twojego serwera źródłowego:
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const url = request.nextUrl;
// Negocjacja treści oparta na urządzeniu
const ua = request.headers.get("user-agent") ?? "";
const isMobile = /mobile|android|iphone/i.test(ua);
response.headers.set("x-device-type", isMobile ? "mobile" : "desktop");
// Flagi funkcji z ciasteczka
const flags = request.cookies.get("feature-flags")?.value;
if (flags) {
response.headers.set("x-feature-flags", flags);
}
// Wykrywanie lokalizacji dla 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;
}Gdzie Edge zawodzi#
To jest sekcja, którą strony marketingowe pomijają. Napotkałem każdy z tych problemów.
1. Połączenia z bazą danych#
To jest ten duży problem. Tradycyjne bazy danych (PostgreSQL, MySQL) używają trwałych połączeń TCP. Serwer Node.js otwiera pulę połączeń przy starcie i ponownie ich używa między żądaniami. Wydajne, sprawdzone, dobrze zrozumiane.
Funkcje edge nie mogą tego zrobić. Każdy izolat jest efemeryczny. Nie ma fazy „startu" gdzie otwierasz połączenia. Nawet gdybyś mógł otworzyć połączenie, izolat może zostać zrecyklowany po jednym żądaniu, marnując czas konfiguracji połączenia.
// ❌ Ten wzorzec fundamentalnie nie działa na edge
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // Pula 10 połączeń
});
// Każde wywołanie edge:
// 1. Tworzy nową Pulę (nie może niezawodnie ponownie użyć między wywołaniami)
// 2. Otwiera połączenie TCP do bazy danych (która jest w us-east-1, nie na edge)
// 3. Robi handshake TLS z bazą danych
// 4. Uruchamia zapytanie
// 5. Odrzuca połączenie gdy izolat jest recyclowany
// Nawet z usługami poolingu połączeń jak PgBouncer,
// wciąż płacisz opóźnienie sieciowe z edge → źródłowa baza danychProblem podróży do bazy danych jest fundamentalny. Twoja baza danych jest w jednym regionie. Twoja funkcja edge jest w 300 regionach. Każde zapytanie do bazy z edge musi podróżować z lokalizacji edge do regionu bazy danych i z powrotem. Dla użytkownika w Tokio trafiającego na węzeł edge w Tokio, ale Twoja baza danych jest w Virginii:
Funkcja edge w Tokio
→ Zapytanie do PostgreSQL w Virginii: ~140ms podróż w obie strony
→ Drugie zapytanie: ~140ms więcej
→ Razem: 280ms tylko na dwa zapytania
Funkcja Node.js w Virginii (ten sam region co baza)
→ Zapytanie do PostgreSQL: ~1ms podróż w obie strony
→ Drugie zapytanie: ~1ms więcej
→ Razem: 2ms na dwa zapytania
Funkcja edge jest 140x wolniejsza dla operacji bazodanowych w tym scenariuszu. Nie ma znaczenia, że funkcja edge wystartowała szybciej — podróże do bazy danych dominują nad wszystkim.
Dlatego istnieją proxy bazodanowe oparte na HTTP (sterownik serverless Neon, sterownik fetch-based PlanetScale, REST API Supabase). Działają, ale wciąż wykonują żądania HTTP do bazy danych w jednym regionie. Rozwiązują problem „nie można użyć TCP", ale nie problem „baza danych jest daleko".
// ✅ To działa na edge (dostęp do bazy oparty na HTTP)
// Ale wciąż jest wolne jeśli baza jest daleko od węzła edge
import { neon } from "@neondatabase/serverless";
export const runtime = "edge";
export async function GET(request: Request) {
const sql = neon(process.env.DATABASE_URL!);
// To wykonuje żądanie HTTP do Twojej bazy Neon
// Działa, ale opóźnienie zależy od odległości do regionu bazy
const posts = await sql`SELECT * FROM posts WHERE published = true LIMIT 10`;
return Response.json(posts);
}2. Długotrwałe zadania#
Funkcje edge mają limity czasu CPU, zazwyczaj 10-50ms rzeczywistego czasu obliczeniowego. Czas zegara jest bardziej łaskawy (zwykle 30 sekund), ale operacje intensywnie korzystające z CPU szybko osiągną limit:
// ❌ Te operacje przekroczą limity czasu CPU na edge
export const runtime = "edge";
export async function POST(request: Request) {
const data = await request.json();
// Przetwarzanie obrazów — intensywne CPU
// (Dodatkowo nie można użyć sharp bo to natywny moduł)
const processed = heavyImageProcessing(data.image);
// Generowanie PDF — intensywne CPU + wymaga API Node.js
const pdf = generatePDF(data.content);
// Duża transformacja danych
const result = data.items // 100 000 elementów
.map(transform)
.filter(validate)
.sort(compare)
.reduce(aggregate, {});
return Response.json(result);
}Jeśli Twoja funkcja potrzebuje więcej niż kilka milisekund czasu CPU, należy do regionalnego serwera Node.js. Kropka.
3. Zależności wyłącznie Node.js#
Ten problem zaskakuje ludzi. Zaskakująco wiele pakietów npm zależy od wbudowanych modułów Node.js:
// ❌ Te pakiety nie będą działać na edge
import bcrypt from "bcrypt"; // Natywne bindowanie C++
import sharp from "sharp"; // Natywne bindowanie C++
import puppeteer from "puppeteer"; // Wymaga systemu plików + child_process
import nodemailer from "nodemailer"; // Wymaga modułu net
import { readFile } from "fs/promises"; // API systemu plików Node.js
import mongoose from "mongoose"; // Połączenia TCP + API Node.js
// ✅ Alternatywy kompatybilne z edge
import { hashSync } from "bcryptjs"; // Czysta implementacja JS (wolniejsza)
// Dla obrazów: użyj osobnej usługi lub API
// Dla emaili: użyj API email opartego na HTTP (Resend, SendGrid REST)
// Dla baz danych: użyj klientów opartych na HTTPPrzed przeniesieniem czegokolwiek na edge sprawdź każdą zależność. Jedno require("fs") ukryte trzy poziomy głęboko w drzewie zależności spowoduje crash Twojej funkcji edge w runtime — nie w czasie budowania. Wdrożysz, wszystko wygląda dobrze, a potem pierwsze żądanie trafia na tę ścieżkę kodu i dostajesz zagadkowy błąd.
4. Duże rozmiary bundli#
Platformy edge mają ścisłe limity rozmiaru bundli:
- Cloudflare Workers: 1MB (darmowy), 5MB (płatny)
- Vercel Edge Functions: 4MB (skompresowane)
- Deno Deploy: 20MB
To brzmi jak dużo dopóki nie zaimportujesz biblioteki komponentów UI, biblioteki walidacji i biblioteki dat. Kiedyś miałem middleware edge, który spuchł do 3.5MB, ponieważ importowałem z pliku barrel, który wciągał cały katalog @/components.
// ❌ Importy z plików barrel mogą wciągnąć zdecydowanie za dużo
import { validateEmail } from "@/lib/utils";
// Jeśli utils.ts re-eksportuje z 20 innych modułów, wszystkie zostaną zbundlowane
// ✅ Importuj bezpośrednio ze źródła
import { validateEmail } from "@/lib/validators/email";5. Streaming i WebSockety#
Funkcje edge mogą robić odpowiedzi strumieniowe (Web Streams API), ale długotrwałe połączenia WebSocket to inna historia. Chociaż niektóre platformy wspierają WebSockety na edge (Cloudflare Workers, Deno Deploy), efemeryczna natura funkcji edge czyni je słabym wyborem dla stanowych, długotrwałych połączeń.
Środowisko uruchomieniowe Edge w Next.js#
Next.js ułatwia przejście na środowisko uruchomieniowe edge na bazie poszczególnych tras. Nie musisz iść na całość — wybierasz dokładnie, które trasy działają na edge.
Middleware (Zawsze Edge)#
Middleware Next.js zawsze działa na edge. To jest zamierzone — middleware przechwytuje każde pasujące żądanie, więc musi być szybki i globalnie rozproszony:
// middleware.ts — zawsze działa na edge, nie wymaga opt-in
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// To działa przed każdym pasującym żądaniem
// Utrzymuj szybko — bez wywołań bazy danych, bez ciężkich obliczeń
return NextResponse.next();
}
export const config = {
// Uruchamiaj tylko na konkretnych ścieżkach
matcher: [
"/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
],
};Trasy API na Edge#
Każdy handler trasy może przejść na środowisko uruchomieniowe edge:
// app/api/hello/route.ts
export const runtime = "edge"; // Ta jedna linia zmienia środowisko uruchomieniowe
export async function GET(request: Request) {
return Response.json({
message: "Hello from the edge",
region: process.env.VERCEL_REGION ?? "unknown",
timestamp: Date.now(),
});
}Trasy stron na Edge#
Nawet całe strony mogą renderować się na edge, choć zastanowiłbym się dobrze zanim to zrobię:
// app/dashboard/page.tsx
export const runtime = "edge";
export default async function DashboardPage() {
// Pamiętaj: brak API Node.js tutaj
// Pobieranie danych musi używać fetch() lub klientów kompatybilnych z 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>Dashboard</h1>
{/* renderuj dane */}
</main>
);
}Co jest dostępne w środowisku uruchomieniowym Edge#
Oto praktyczne odniesienie co możesz, a czego nie możesz używać:
// ✅ Dostępne na edge
fetch() // Żądania HTTP
Request / Response // Standardowe żądanie/odpowiedź webowe
Headers // Nagłówki HTTP
URL / URLSearchParams // Parsowanie URL
TextEncoder / TextDecoder // Kodowanie ciągów
crypto.subtle // Operacje kryptograficzne (podpisywanie, hashowanie)
crypto.randomUUID() // Generowanie UUID
crypto.getRandomValues() // Kryptograficzne liczby losowe
structuredClone() // Głębokie klonowanie
atob() / btoa() // Kodowanie/dekodowanie Base64
setTimeout() / setInterval() // Timery (ale pamiętaj o limitach CPU)
console.log() // Logowanie
ReadableStream / WritableStream // Streaming
AbortController / AbortSignal // Anulowanie żądań
URLPattern // Dopasowywanie wzorców URL
// ❌ NIE dostępne na edge
require() // CommonJS (używaj import)
fs / path / os // Wbudowane moduły Node.js
process.exit() // Kontrola procesu
Buffer // Użyj Uint8Array zamiast tego
__dirname / __filename // Użyj import.meta.url
setImmediate() // Nie jest standardem webowymUwierzytelnianie na Edge: Pełny wzorzec#
Chcę zagłębić się w uwierzytelnianie, ponieważ jest to jeden z najbardziej wpływowych przypadków użycia edge, ale łatwo jest to zrobić źle.
Wzorzec, który działa to: weryfikuj token na edge, przekazuj zaufane claimy dalej, nigdy nie dotykaj bazy danych w middleware.
// lib/edge-auth.ts — Narzędzia auth kompatybilne z 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 sekund tolerancji odchylenia zegara
});
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 — Middleware uwierzytelniający
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;
// Pomiń auth dla publicznych ścieżek
if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith("/api/public"))) {
return NextResponse.next();
}
// Wyciągnij token
const token = request.cookies.get("auth-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Weryfikuj token (czysta kryptografia — bez wywołania bazy danych)
const payload = await verifyToken(token);
if (!payload) {
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("auth-token");
return response;
}
// Kontrola dostępu oparta na rolach
if (ADMIN_PATHS.some((p) => pathname.startsWith(p)) && payload.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
// Przekaż zweryfikowane informacje o użytkowniku do serwera źródłowego jako zaufane nagłówki
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);
// Sygnalizuj jeśli token wymaga odświeżenia
if (isTokenExpiringSoon(payload)) {
response.headers.set("x-token-refresh", "true");
}
return response;
}// app/api/profile/route.ts — Serwer źródłowy odczytuje zaufane nagłówki
export async function GET(request: Request) {
// Te nagłówki zostały ustawione przez middleware edge po weryfikacji JWT
// Są zaufane, ponieważ pochodzą z naszej własnej infrastruktury
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 });
}
// Teraz możemy odpytać bazę — jesteśmy na serwerze źródłowym,
// tuż obok bazy danych, z pulą połączeń
const user = await db.user.findUnique({ where: { id: userId } });
return Response.json(user);
}Kluczowe spostrzeżenie: edge robi szybką część (weryfikacja kryptograficzna), a serwer źródłowy robi wolną część (zapytania do bazy danych). Każdy działa tam, gdzie jest najbardziej wydajny.
Jedno ważne zastrzeżenie: to działa tylko dla JWT. Jeśli Twój system uwierzytelniania wymaga lookupu w bazie danych przy każdym żądaniu (jak uwierzytelnianie oparte na sesjach z ciasteczkiem ID sesji), edge nie pomoże — wciąż musiałbyś wywołać bazę danych, co oznacza podróż do regionu serwera źródłowego.
Cachowanie na Edge#
Cachowanie na edge to miejsce, gdzie sprawy stają się interesujące. Węzły edge mogą cachować odpowiedzi, co oznacza, że kolejne żądania do tego samego URL są serwowane bezpośrednio z edge bez dotykania Twojego serwera źródłowego w ogóle.
Cache-Control zrobiony dobrze#
// 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 na CDN przez 60 sekund
// Serwuj stałe podczas rewalidacji przez do 5 minut
// Klient może cachować przez 10 sekund
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300, max-age=10",
// Vary po tych nagłówkach, żeby różne warianty miały różne wpisy cache
Vary: "Accept-Language, Accept-Encoding",
// Tag cache specyficzny dla CDN do celowanej inwalidacji
"Cache-Tag": `products,category-${category}`,
},
});
}Wzorzec stale-while-revalidate jest szczególnie potężny na edge. Oto co się dzieje:
- Pierwsze żądanie: Edge pobiera z serwera źródłowego, cachuje odpowiedź, zwraca ją
- Żądania w ciągu 60 sekund: Edge serwuje z cache (0ms opóźnienia źródłowego)
- Żądanie w 61-360 sekundzie: Edge serwuje stałą wersję z cache natychmiast, ale pobiera świeżą wersję z serwera źródłowego w tle
- Po 360 sekundach: Cache jest w pełni wygasły, następne żądanie idzie do serwera źródłowego
Twoi użytkownicy prawie zawsze dostają odpowiedź z cache. Kompromis świeżości jest jawny i konfigurowalny.
Edge Config dla dynamicznej konfiguracji#
Edge Config od Vercela (i podobne usługi od innych platform) pozwala przechowywać konfigurację klucz-wartość, która jest replikowana do każdej lokalizacji edge. Jest to niesamowicie przydatne dla flag funkcji, reguł przekierowań i konfiguracji testów A/B, które chcesz aktualizować bez ponownego wdrożenia:
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
// Odczyty Edge Config są ekstremalnie szybkie (~1ms) ponieważ
// dane są replikowane do każdej lokalizacji edge
const maintenanceMode = await get<boolean>("maintenance_mode");
if (maintenanceMode) {
return NextResponse.rewrite(new URL("/maintenance", request.url));
}
// Flagi funkcji
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));
}
// Dynamiczne przekierowania (aktualizuj przekierowania bez ponownego wdrożenia)
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();
}To jest prawdziwy game-changer. Przed Edge Config zmiana flagi funkcji oznaczała zmianę kodu i ponowne wdrożenie. Teraz aktualizujesz wartość JSON w dashboardzie i propaguje się globalnie w ciągu sekund.
Prawdziwa matematyka wydajności#
Zróbmy uczciwą matematykę zamiast marketingowej. Porównam trzy architektury dla typowego endpointu API, który musi zapytać bazę danych:
Scenariusz: API profilu użytkownika (2 zapytania do bazy)#
Architektura A: Tradycyjny regionalny Node.js
Użytkownik w Tokio → Serwer w Virginii: 140ms
+ Zapytanie DB 1 (ten sam region): 2ms
+ Zapytanie DB 2 (ten sam region): 2ms
+ Przetwarzanie: 5ms
= Razem: ~149ms
Architektura B: Funkcja Edge z bazą danych HTTP
Użytkownik w Tokio → Edge w Tokio: 5ms
+ Zapytanie DB 1 (HTTP do Virginii): 145ms
+ Zapytanie DB 2 (HTTP do Virginii): 145ms
+ Przetwarzanie: 3ms
= Razem: ~298ms ← WOLNIEJSZA niż regionalna
Architektura C: Funkcja Edge z regionalną bazą danych (replika odczytu)
Użytkownik w Tokio → Edge w Tokio: 5ms
+ Zapytanie DB 1 (HTTP do repliki w Tokio): 8ms
+ Zapytanie DB 2 (HTTP do repliki w Tokio): 8ms
+ Przetwarzanie: 3ms
= Razem: ~24ms ← Najszybsza, ale wymaga wieloregionalnej DB
Architektura D: Edge dla auth + regionalne dla danych
Użytkownik w Tokio → Middleware edge w Tokio: 5ms (weryfikacja JWT)
→ Serwer w Virginii: 140ms
+ Zapytanie DB 1 (ten sam region): 2ms
+ Zapytanie DB 2 (ten sam region): 2ms
+ Przetwarzanie: 5ms
= Razem: ~154ms
(Ale auth jest już zweryfikowany — serwer nie musi ponownie weryfikować)
(A nieautoryzowane żądania są blokowane na edge — nigdy nie docierają do serwera)
Wnioski:
- Edge + źródłowa baza danych = często wolniejsze niż użycie samego serwera regionalnego
- Edge + wieloregionalna baza danych = najszybsze ale najdroższe i najbardziej złożone
- Edge do strażnikowania + regionalne dla danych = najlepszy pragmatyczny balans
- Czysty edge (bez bazy danych) = nie do pobicia dla rzeczy jak przekierowania i sprawdzanie auth
Architektura D to ta, której używam w większości projektów. Edge obsługuje to, w czym jest dobry (szybkie decyzje, auth, routing), a regionalny serwer Node.js obsługuje to, w czym jest dobry (zapytania do bazy danych, ciężkie obliczenia).
Kiedy Edge naprawdę wygrywa: Operacje bez bazy danych#
Matematyka kompletnie się odwraca, kiedy nie ma bazy danych w grze:
Przekierowanie (edge):
Użytkownik w Tokio → Edge w Tokio → odpowiedź przekierowania: ~5ms
Przekierowanie (regionalne):
Użytkownik w Tokio → Serwer w Virginii → odpowiedź przekierowania: ~280ms
Statyczna odpowiedź API (edge + cache):
Użytkownik w Tokio → Edge w Tokio → odpowiedź z cache: ~5ms
Statyczna odpowiedź API (regionalne):
Użytkownik w Tokio → Serwer w Virginii → odpowiedź: ~280ms
Blokowanie botów (edge):
Zły bot gdziekolwiek → Edge (najbliższy) → odpowiedź 403: ~5ms
(Bot nigdy nie dociera do Twojego serwera źródłowego)
Blokowanie botów (regionalne):
Zły bot gdziekolwiek → Serwer w Virginii → odpowiedź 403: ~280ms
(Bot wciąż zużywał zasoby serwera źródłowego)
Dla operacji, które nie potrzebują bazy danych, edge jest 20-50x szybszy. To nie jest marketing — to fizyka.
Mój framework decyzyjny#
Po roku pracy z funkcjami edge w produkcji, oto schemat blokowy, którego używam dla każdego nowego endpointu lub kawałka logiki:
Krok 1: Czy wymaga API Node.js?#
Jeśli importuje fs, net, child_process lub jakikolwiek natywny moduł — regionalny Node.js. Bez dyskusji.
Krok 2: Czy wymaga zapytań do bazy danych?#
Jeśli tak, i nie masz replik odczytu blisko użytkowników — regionalny Node.js (w tym samym regionie co Twoja baza danych). Podróże do bazy danych będą dominować.
Jeśli tak, i masz globalnie rozproszone repliki odczytu — Edge może działać, używając klientów bazodanowych opartych na HTTP.
Krok 3: Czy to decyzja o żądaniu (routing, auth, przekierowanie)?#
Jeśli tak — Edge. To jest słodki punkt. Podejmujesz szybką decyzję, która określa co dzieje się z żądaniem zanim dotrze do serwera źródłowego.
Krok 4: Czy odpowiedź jest cachowalana?#
Jeśli tak — Edge z odpowiednimi nagłówkami Cache-Control. Nawet jeśli pierwsze żądanie idzie do serwera źródłowego, kolejne żądania są serwowane z cache edge.
Krok 5: Czy to jest intensywne CPU?#
Jeśli wymaga znaczących obliczeń (przetwarzanie obrazów, generowanie PDF, duże transformacje danych) — regionalny Node.js.
Krok 6: Jak wrażliwe na opóźnienia to jest?#
Jeśli to zadanie w tle lub webhook — regionalny Node.js. Nikt na to nie czeka. Jeśli to żądanie skierowane do użytkownika, gdzie liczy się każda ms — Edge, jeśli spełnia pozostałe kryteria.
Ściąga#
// ✅ IDEALNE dla edge
// - Middleware (auth, przekierowania, przepisywanie, nagłówki)
// - Logika geolokalizacji
// - Przypisywanie testów A/B
// - Wykrywanie botów / reguły WAF
// - Odpowiedzi API przyjazne dla cache
// - Sprawdzanie flag funkcji
// - Odpowiedzi preflight CORS
// - Statyczne transformacje danych (bez DB)
// - Weryfikacja podpisów webhooków
// ❌ ZOSTAWIĆ na regionalnym Node.js
// - Operacje CRUD na bazie danych
// - Przesyłanie / przetwarzanie plików
// - Manipulacja obrazami
// - Generowanie PDF
// - Wysyłanie emaili (użyj API HTTP, ale wciąż regionalne)
// - Serwery WebSocket
// - Zadania w tle / kolejki
// - Cokolwiek używające natywnych pakietów npm
// - Strony SSR z zapytaniami do bazy
// - Resolvery GraphQL odpytujące bazy danych
// 🤔 TO ZALEŻY
// - Uwierzytelnianie (edge dla JWT, regionalne dla sesji-DB)
// - Trasy API (edge jeśli bez DB, regionalne jeśli z DB)
// - Strony renderowane serwerowo (edge jeśli dane z cache/fetch, regionalne jeśli DB)
// - Funkcje czasu rzeczywistego (edge dla początkowego auth, regionalne dla trwałych połączeń)Co naprawdę uruchamiam na Edge#
Dla tej strony, oto podział:
Edge (middleware):
- Wykrywanie lokalizacji i przekierowanie
- Filtrowanie botów
- Nagłówki bezpieczeństwa (CSP, HSTS, itd.)
- Logowanie dostępu
- Ograniczanie częstotliwości (podstawowe)
Regionalny Node.js:
- Renderowanie treści bloga (przetwarzanie MDX wymaga API Node.js przez Velite)
- Trasy API dotykające Redis
- Generowanie obrazów OG (wymaga więcej czasu CPU)
- Generowanie kanału RSS
Statyczne (brak środowiska uruchomieniowego w ogóle):
- Strony narzędzi (pre-renderowane w czasie budowania)
- Strony postów bloga (pre-renderowane w czasie budowania)
- Wszystkie obrazy i zasoby (serwowane przez CDN)
Najlepsze środowisko uruchomieniowe to często żadne środowisko uruchomieniowe. Jeśli możesz pre-renderować coś w czasie budowania i serwować jako zasób statyczny, to zawsze będzie szybsze niż jakakolwiek funkcja edge. Edge jest dla rzeczy, które naprawdę muszą być dynamiczne przy każdym żądaniu.
Uczciwe podsumowanie#
Funkcje edge nie zastępują tradycyjnych serwerów. Są uzupełnieniem. Są dodatkowym narzędziem w Twoim architektonicznym zestawie — niezwykle potężnym do właściwych przypadków użycia i aktywnie szkodliwym do niewłaściwych.
Heurystyka, do której wciąż wracam: jeśli Twoja funkcja musi sięgnąć do bazy danych w jednym regionie, umieszczenie funkcji na edge nie pomoże — zaszkodzi. Właśnie dodałeś skok. Funkcja uruchamia się szybciej, ale potem spędza 100ms+ sięgając z powrotem do bazy danych. Wynik netto: wolniejsze niż uruchamianie wszystkiego w jednym regionie.
Ale dla decyzji, które mogą być podejmowane tylko z informacjami zawartymi w samym żądaniu — geolokalizacja, ciasteczka, nagłówki, JWT — edge jest nie do pobicia. Te 5ms odpowiedzi edge to nie syntetyczne benchmarki. Są prawdziwe i Twoi użytkownicy czują różnicę.
Nie przenoś wszystkiego na edge. Nie trzymaj wszystkiego z dala od edge. Umieść każdy element logiki tam, gdzie fizyka mu sprzyja.