Edge Functions: Що це, коли використовувати і коли ні
Edge runtime, V8 isolates, міф про cold start, гео-роутинг, A/B тестування, автентифікація на edge та чому я деякі речі переніс назад на Node.js. Збалансований погляд на edge computing.
Слово «edge» зустрічається повсюди. Vercel про нього говорить. Cloudflare говорить. Deno говорить. Кожна конференційна доповідь про веб-продуктивність неминуче згадує «виконання на edge» так, ніби це магічне заклинання, що робить ваш застосунок швидким.
Я повірив у це. Я переніс middleware, API-маршрути і навіть деяку логіку рендерингу на edge runtime. Деякі з цих рішень були геніальними. Інші я тихенько повернув назад на Node.js через три тижні, після того як о другій ночі дебажив помилки пулу з'єднань.
Ця стаття — збалансована версія цієї історії: що таке edge насправді, де він реально блищить, де абсолютно не підходить, і як я вирішую, який runtime використовувати для кожної частини свого застосунку.
Що таке Edge?#
Почнімо з географії. Коли хтось відвідує ваш сайт, запит подорожує від його пристрою через провайдера, через інтернет до вашого сервера, обробляється, і відповідь подорожує весь шлях назад. Якщо ваш сервер знаходиться в us-east-1 (Вірджинія), а ваш користувач — у Токіо, цей маршрут туди-назад покриває приблизно 14 000 км. На швидкості світла через оптоволокно це близько 70 мс лише за фізикою — в один бік. Додайте DNS-резолвінг, TLS-рукостискання та будь-який час обробки, і ви легко дивитесь на 200-400 мс, перш ніж ваш користувач побачить хоча б один байт.
«Edge» означає виконання вашого коду на серверах, розподілених глобально — тих самих CDN-вузлах, що завжди обслуговували статичні ресурси, але тепер вони також можуть виконувати вашу логіку. Замість одного origin-сервера у Вірджинії ваш код працює на 300+ локаціях по всьому світу. Користувач у Токіо потрапляє на сервер у Токіо. Користувач у Парижі потрапляє на сервер у Парижі.
Математика затримки проста і переконлива:
Традиційна архітектура (один origin):
Токіо → Вірджинія: ~140 мс туди-назад (лише фізика)
+ TLS-рукостискання: ще ~140 мс (ще один маршрут)
+ Обробка: 20-50 мс
Разом: ~300-330 мс
Edge (локальний PoP):
Токіо → Edge-вузол у Токіо: ~5 мс туди-назад
+ TLS-рукостискання: ще ~5 мс
+ Обробка: 5-20 мс
Разом: ~15-30 мс
Це покращення у 10-20 разів для початкової відповіді. Це реально, це вимірювано, і для певних операцій це трансформаційно.
Але ось що маркетинг замовчує: edge — це не повне серверне середовище. Це щось фундаментально інше.
V8 Isolates проти Node.js#
Традиційний Node.js працює в повноцінному процесі операційної системи. Він має доступ до файлової системи, може відкривати TCP-з'єднання, може породжувати дочірні процеси, може читати змінні оточення як потік, може робити по суті все, що може робити процес Linux.
Edge-функції не працюють на Node.js. Вони працюють на V8 isolates — тому самому JavaScript-рушії, що живить Chrome, але урізаному до ядра. Уявіть V8 isolate як легкий sandbox:
// Це працює в Node.js, але НЕ на edge
import fs from "fs";
import { createConnection } from "net";
import { execSync } from "child_process";
const file = fs.readFileSync("/etc/hosts"); // ❌ Немає файлової системи
const conn = createConnection({ port: 5432 }); // ❌ Немає сирого TCP
const result = execSync("ls -la"); // ❌ Немає дочірніх процесів
process.env.DATABASE_URL; // ⚠️ Доступно, але статичне, встановлюється при деплоїЩо ви МАЄТЕ на edge — це поверхня Web API, ті самі API, що доступні в браузері:
// Все це працює на 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());Обмеження реальні та жорсткі:
- Пам'ять: 128 МБ на ізолят (Cloudflare Workers), 256 МБ на деяких платформах
- Час CPU: 10-50 мс фактичного часу CPU (не wall clock time —
await fetch()не рахується, алеJSON.parse()на 5 МБ payload рахується) - Немає нативних модулів: Все, що потребує C++ binding (bcrypt, sharp, canvas) — виключено
- Немає постійних з'єднань: Ви не можете тримати відкрите з'єднання з базою даних між запитами
- Обмеження розміру бандлу: Зазвичай 1-5 МБ для всього worker-скрипту
Це не Node.js на CDN. Це інший runtime з іншою ментальною моделлю.
Cold Starts: Міф і реальність#
Ви, мабуть, чули, що edge-функції мають «нульові cold starts». Це... здебільшого правда, і порівняння дійсно вражає.
Традиційна контейнерна серверless-функція (AWS Lambda, Google Cloud Functions) працює так:
- Запит надходить
- Платформа надає контейнер (якщо немає доступного)
- Контейнер завантажує ОС
- Runtime ініціалізується (Node.js, Python тощо)
- Ваш код завантажується та ініціалізується
- Запит обробляється
Кроки 2-5 — це cold start. Для Node.js Lambda це зазвичай 200-500 мс. Для Java Lambda — 2-5 секунд. Для .NET Lambda — 500 мс-1,5 с.
V8 isolates працюють інакше:
- Запит надходить
- Платформа створює новий V8 isolate (або перевикористовує теплий)
- Ваш код завантажується (він вже скомпільований у байткод при деплої)
- Запит обробляється
Кроки 2-3 займають менше 5 мс. Часто менше 1 мс. Ізолят — це не контейнер: немає ОС для завантаження, немає runtime для ініціалізації. V8 створює свіжий ізолят за мікросекунди. Фраза «нульовий cold start» — це маркетингова мова, але реальність (старт менше 5 мс) достатньо близька до нуля, щоб це не мало значення для більшості випадків використання.
Але ось коли cold starts все ж дають про себе знати на edge:
Великі бандли. Якщо ваша edge-функція підтягує 2 МБ залежностей, цей код все одно потрібно завантажити й розпарсити. Я вивчив це на власному досвіді, коли зібрав бібліотеку валідації та бібліотеку форматування дат в edge middleware. Cold start зріс із 2 мс до 40 мс. Все ще швидко, але не «нуль».
Рідкісні локації. Edge-провайдери мають сотні PoP, але не всі PoP тримають ваш код теплим. Якщо ви отримуєте один запит на годину з Найробі, цей ізолят утилізується між запитами. Наступний запит знову платить за старт.
Декілька ізолятів на запит. Якщо ваша edge-функція викликає іншу edge-функцію (або якщо middleware і API-маршрут обидва edge), ви можете розкручувати кілька ізолятів для одного запиту користувача.
Практична порада: тримайте бандли edge-функцій маленькими. Імпортуйте лише те, що потрібно. Агресивно робіть tree-shake. Чим менший бандл, тим швидший cold start, тим більше обіцянка «нульового cold start» виконується.
// ❌ Не робіть так на edge
import dayjs from "dayjs";
import * as yup from "yup";
import lodash from "lodash";
// ✅ Робіть так — використовуйте вбудовані API
const date = new Date().toISOString();
const isValid = typeof input === "string" && input.length < 200;
const unique = [...new Set(items)];Ідеальні випадки використання Edge-функцій#
Після тривалих експериментів я знайшов чіткий патерн: edge-функції відмінно працюють, коли вам потрібно прийняти швидке рішення щодо запиту до того, як він досягне вашого origin-сервера. Вони — привратники, маршрутизатори та трансформатори, а не серверні застосунки.
1. Редиректи на основі геолокації#
Це кіллер-кейс. Запит потрапляє на найближчий edge-вузол, який вже знає, де знаходиться користувач. Не потрібен API-виклик, не потрібна база даних IP-пошуку — платформа надає гео-дані:
// middleware.ts — працює на edge при кожному запиті
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";
// Редирект на специфічний для країни магазин
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));
}
}
// Додати гео-заголовки для подальшого використання
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;
}Це виконується менш ніж за 5 мс, прямо поруч із користувачем. Альтернатива — надсилання запиту до origin-сервера лише для IP-пошуку та редиректу назад — коштувала б 100-300 мс для користувачів далеко від вашого origin.
2. A/B тестування без мерехтіння на клієнті#
Клієнтське A/B тестування спричиняє жахливий «спалах оригінального контенту» — користувач бачить версію A на долю секунди, перш ніж JavaScript замінить на версію B. На edge ви можете призначити варіант до того, як сторінка навіть почне рендеритися:
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Перевірити, чи є у користувача вже призначений варіант
const existingVariant = request.cookies.get("ab-variant")?.value;
if (existingVariant) {
// Переписати на правильну сторінку варіанта
const url = request.nextUrl.clone();
url.pathname = `/variants/${existingVariant}${url.pathname}`;
return NextResponse.rewrite(url);
}
// Призначити новий варіант (розподіл 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 днів
httpOnly: true,
sameSite: "lax",
});
return response;
}Користувач ніколи не бачить мерехтіння, тому що перенаправлення відбувається на рівні мережі. Браузер навіть не знає, що це був A/B тест — він просто отримує сторінку варіанта напряму.
3. Верифікація токенів автентифікації#
Якщо ваша автентифікація використовує JWT (і ви не робите пошук сесій в базі даних), edge — ідеальне рішення. Верифікація JWT — це чиста криптографія, база даних не потрібна:
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",
});
// Передати інформацію про користувача далі як заголовки
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 {
// Токен прострочений або недійсний
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("session-token");
return response;
}
}Цей патерн потужний: edge middleware верифікує токен і передає інформацію про користувача вашому origin як довірені заголовки. Вашим API-маршрутам не потрібно знову верифікувати токен — вони просто читають request.headers.get("x-user-id").
4. Виявлення ботів та обмеження частоти запитів#
Edge-функції можуть блокувати небажаний трафік ще до того, як він досягне вашого origin:
import { NextRequest, NextResponse } from "next/server";
// Простий rate limiter в пам'яті (для кожної 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") ?? "";
// Блокувати відомих поганих ботів
const badBots = ["AhrefsBot", "SemrushBot", "MJ12bot", "DotBot"];
if (badBots.some((bot) => ua.includes(bot))) {
return new NextResponse("Forbidden", { status: 403 });
}
// Просте обмеження частоти
const now = Date.now();
const windowMs = 60_000; // 1 хвилина
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 });
}
// Періодичне очищення для запобігання витоку пам'яті
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();
}Одне застереження: карта обмеження частоти вище — це per-ізолят, per-локація. Якщо у вас 300 edge-локацій, кожна з них має свою карту. Для суворого обмеження потрібне розподілене сховище на кшталт Upstash Redis або Cloudflare Durable Objects. Але для грубого запобігання зловживанням per-локаційні ліміти працюють напрочуд добре.
5. Переписування запитів та заголовки персоналізації#
Edge-функції чудово підходять для трансформації запитів перед тим, як вони досягнуть origin:
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const url = request.nextUrl;
// Узгодження контенту на основі пристрою
const ua = request.headers.get("user-agent") ?? "";
const isMobile = /mobile|android|iphone/i.test(ua);
response.headers.set("x-device-type", isMobile ? "mobile" : "desktop");
// Прапори функцій з cookie
const flags = request.cookies.get("feature-flags")?.value;
if (flags) {
response.headers.set("x-feature-flags", flags);
}
// Визначення локалі для 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;
}Де Edge не справляється#
Це розділ, який маркетингові сторінки пропускають. Я стикався з кожною з цих стін.
1. З'єднання з базами даних#
Це головна проблема. Традиційні бази даних (PostgreSQL, MySQL) використовують постійні TCP-з'єднання. Node.js-сервер відкриває пул з'єднань при старті та перевикористовує ці з'єднання між запитами. Ефективно, перевірено, добре зрозуміло.
Edge-функції не можуть цього зробити. Кожен ізолят ефемерний. Немає фази «стартування», де ви відкриваєте з'єднання. Навіть якби ви могли відкрити з'єднання, ізолят може бути утилізований після одного запиту, марнуючи час налаштування з'єднання.
// ❌ Цей патерн фундаментально не працює на edge
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // Пул з 10 з'єднань
});
// Кожне виклик edge буде:
// 1. Створювати новий Pool (не може надійно перевикористовувати між викликами)
// 2. Відкривати TCP-з'єднання до бази даних (яка в us-east-1, не на edge)
// 3. Робити TLS-рукостискання з базою даних
// 4. Виконувати запит
// 5. Відкидати з'єднання, коли ізолят утилізується
// Навіть із сервісами пулінгу з'єднань типу PgBouncer,
// ви все одно платите мережеву затримку від edge → origin бази данихПроблема з round-trip до бази даних фундаментальна. Ваша база даних в одному регіоні. Ваша edge-функція у 300 регіонах. Кожен запит до бази з edge повинен подорожувати від edge-локації до регіону бази даних і назад. Для користувача в Токіо, що потрапляє на edge-вузол у Токіо, але ваша база в Вірджинії:
Edge-функція в Токіо
→ Запит до PostgreSQL у Вірджинії: ~140 мс туди-назад
→ Другий запит: ще ~140 мс
→ Разом: 280 мс лише для двох запитів
Node.js-функція у Вірджинії (той самий регіон, що й БД)
→ Запит до PostgreSQL: ~1 мс туди-назад
→ Другий запит: ще ~1 мс
→ Разом: 2 мс для двох запитів
Edge-функція у 140 разів повільніша для операцій з базою даних у цьому сценарії. Не має значення, що edge-функція стартувала швидше — round-trip до бази даних домінує над усім.
Ось чому існують HTTP-based проксі бази даних (serverless-драйвер Neon, fetch-based драйвер PlanetScale, REST API Supabase). Вони працюють, але все одно роблять HTTP-запити до бази в одному регіоні. Вони вирішують проблему «не можу використовувати TCP», але не проблему «база даних далеко».
// ✅ Це працює на edge (HTTP-based доступ до бази)
// Але все ще повільно, якщо база далеко від edge-вузла
import { neon } from "@neondatabase/serverless";
export const runtime = "edge";
export async function GET(request: Request) {
const sql = neon(process.env.DATABASE_URL!);
// Це робить HTTP-запит до вашої Neon бази
// Працює, але затримка залежить від відстані до регіону бази
const posts = await sql`SELECT * FROM posts WHERE published = true LIMIT 10`;
return Response.json(posts);
}2. Довготривалі задачі#
Edge-функції мають обмеження часу CPU, зазвичай 10-50 мс реального часу обчислень. Wall clock time більш щедрий (зазвичай 30 секунд), але CPU-інтенсивні операції швидко досягають ліміту:
// ❌ Це перевищить ліміти часу CPU на edge
export const runtime = "edge";
export async function POST(request: Request) {
const data = await request.json();
// Обробка зображень — CPU-інтенсивна
// (Також не може використовувати sharp, бо це нативний модуль)
const processed = heavyImageProcessing(data.image);
// Генерація PDF — CPU-інтенсивна + потребує Node.js API
const pdf = generatePDF(data.content);
// Велика трансформація даних
const result = data.items // 100 000 елементів
.map(transform)
.filter(validate)
.sort(compare)
.reduce(aggregate, {});
return Response.json(result);
}Якщо вашій функції потрібно більше кількох мілісекунд часу CPU, їй місце на регіональному Node.js-сервері. Крапка.
3. Залежності тільки для Node.js#
Цей пункт застає людей зненацька. Дивовижна кількість npm-пакетів залежить від вбудованих модулів Node.js:
// ❌ Ці пакети не працюватимуть на edge
import bcrypt from "bcrypt"; // Нативний C++ binding
import sharp from "sharp"; // Нативний C++ binding
import puppeteer from "puppeteer"; // Потребує filesystem + child_process
import nodemailer from "nodemailer"; // Потребує модуль net
import { readFile } from "fs/promises"; // Node.js filesystem API
import mongoose from "mongoose"; // TCP-з'єднання + Node.js API
// ✅ Edge-сумісні альтернативи
import { hashSync } from "bcryptjs"; // Чиста JS-реалізація (повільніша)
// Для зображень: використовуйте окремий сервіс або API
// Для email: використовуйте HTTP-based email API (Resend, SendGrid REST)
// Для бази даних: використовуйте HTTP-based клієнтиПерш ніж переносити що-небудь на edge, перевірте кожну залежність. Один require("fs"), заритий на три рівні вглиб у вашому дереві залежностей, зламає вашу edge-функцію під час виконання — не під час збірки. Ви задеплоїте, все виглядатиме нормально, а потім перший запит потрапить на той шлях коду і ви отримаєте загадкову помилку.
4. Великі розміри бандлів#
Edge-платформи мають суворі обмеження розміру бандлу:
- Cloudflare Workers: 1 МБ (безплатний), 5 МБ (платний)
- Vercel Edge Functions: 4 МБ (стиснений)
- Deno Deploy: 20 МБ
Це здається достатнім, поки ви не зробите import бібліотеки UI-компонентів, бібліотеки валідації та бібліотеки дат. Якось мій edge middleware роздувся до 3,5 МБ, тому що я імпортував з barrel-файлу, який підтягнув весь каталог @/components.
// ❌ Barrel-файлові імпорти можуть підтягнути занадто багато
import { validateEmail } from "@/lib/utils";
// Якщо utils.ts реекспортує з 20 інших модулів, всі вони потрапляють у бандл
// ✅ Імпортуйте безпосередньо з джерела
import { validateEmail } from "@/lib/validators/email";5. Стрімінг та WebSockets#
Edge-функції вміють робити стрімінгові відповіді (Web Streams API), але довготривалі WebSocket-з'єднання — це інша історія. Хоча деякі платформи підтримують WebSockets на edge (Cloudflare Workers, Deno Deploy), ефемерна природа edge-функцій робить їх поганим вибором для stateful довготривалих з'єднань.
Next.js Edge Runtime#
Next.js робить простим вибір edge runtime для кожного маршруту окремо. Вам не потрібно йти all-in — ви вибираєте точно, які маршрути працюють на edge.
Middleware (завжди Edge)#
Next.js middleware завжди працює на edge. Це за замовчуванням — middleware перехоплює кожен відповідний запит, тому має бути швидким і глобально розподіленим:
// middleware.ts — завжди працює на edge, не потребує вибору
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Це виконується перед кожним відповідним запитом
// Тримайте це швидким — без викликів бази, без важких обчислень
return NextResponse.next();
}
export const config = {
// Працює лише на конкретних шляхах
matcher: [
"/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
],
};API-маршрути на Edge#
Будь-який route handler може обрати edge runtime:
// app/api/hello/route.ts
export const runtime = "edge"; // Цей один рядок змінює runtime
export async function GET(request: Request) {
return Response.json({
message: "Hello from the edge",
region: process.env.VERCEL_REGION ?? "unknown",
timestamp: Date.now(),
});
}Сторінкові маршрути на Edge#
Навіть цілі сторінки можуть рендеритися на edge, хоча я б ретельно подумав перед цим:
// app/dashboard/page.tsx
export const runtime = "edge";
export default async function DashboardPage() {
// Пам'ятайте: тут немає Node.js API
// Будь-яке отримання даних повинно використовувати fetch() або 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>
{/* рендер даних */}
</main>
);
}Що доступне в Edge Runtime#
Ось практичний довідник того, що можна і що не можна використовувати:
// ✅ Доступно на edge
fetch() // HTTP-запити
Request / Response // Стандартний веб request/response
Headers // HTTP-заголовки
URL / URLSearchParams // Парсинг URL
TextEncoder / TextDecoder // Кодування рядків
crypto.subtle // Криптографічні операції (підпис, хешування)
crypto.randomUUID() // Генерація UUID
crypto.getRandomValues() // Криптографічні випадкові числа
structuredClone() // Глибоке клонування
atob() / btoa() // Base64 кодування/декодування
setTimeout() / setInterval() // Таймери (але пам'ятайте про ліміти CPU)
console.log() // Логування
ReadableStream / WritableStream // Стрімінг
AbortController / AbortSignal // Скасування запитів
URLPattern // Зіставлення шаблонів URL
// ❌ НЕ доступно на edge
require() // CommonJS (використовуйте import)
fs / path / os // Вбудовані модулі Node.js
process.exit() // Керування процесом
Buffer // Використовуйте Uint8Array замість цього
__dirname / __filename // Використовуйте import.meta.url
setImmediate() // Не є веб-стандартомАвтентифікація на Edge: Повний патерн#
Я хочу заглибитися в автентифікацію, тому що це один із найвпливовіших випадків використання edge, але також легко зробити неправильно.
Патерн, який працює: верифікуйте токен на edge, передавайте довірені claims далі, ніколи не торкайтеся бази даних у middleware.
// lib/edge-auth.ts — 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 секунд толеранції зсуву годинника
});
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 автентифікації
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;
// Пропустити автентифікацію для публічних шляхів
if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith("/api/public"))) {
return NextResponse.next();
}
// Витягти токен
const token = request.cookies.get("auth-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Верифікувати токен (чиста криптографія — без виклику бази)
const payload = await verifyToken(token);
if (!payload) {
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("auth-token");
return response;
}
// Контроль доступу на основі ролей
if (ADMIN_PATHS.some((p) => pathname.startsWith(p)) && payload.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
// Передати верифіковану інформацію про користувача origin як довірені заголовки
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);
// Сигнал, якщо токен потребує оновлення
if (isTokenExpiringSoon(payload)) {
response.headers.set("x-token-refresh", "true");
}
return response;
}// app/api/profile/route.ts — Origin-сервер читає довірені заголовки
export async function GET(request: Request) {
// Ці заголовки були встановлені edge middleware після верифікації JWT
// Їм можна довіряти, бо вони надходять від нашої власної інфраструктури
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 });
}
// Тепер можемо звернутися до бази — ми на origin-сервері,
// прямо поруч з базою даних, з пулом з'єднань
const user = await db.user.findUnique({ where: { id: userId } });
return Response.json(user);
}Ключовий інсайт: edge робить швидку частину (криптографічна верифікація), а origin робить повільну частину (запити до бази). Кожен працює там, де найефективніший.
Одне важливе застереження: це працює лише для JWT. Якщо ваша система автентифікації потребує пошуку в базі при кожному запиті (як session-based автентифікація з session ID cookie), edge не може допомогти — вам все одно потрібно звертатися до бази, а це означає round-trip до origin-регіону.
Кешування на Edge#
Кешування на edge — ось де стає цікаво. Edge-вузли можуть кешувати відповіді, що означає, що подальші запити на той самий URL обслуговуються безпосередньо з edge без звернення до origin взагалі.
Cache-Control зроблений правильно#
// 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: {
// Кешувати на CDN 60 секунд
// Віддавати застаріле під час ревалідації до 5 хвилин
// Клієнт може кешувати 10 секунд
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300, max-age=10",
// Vary за цими заголовками, щоб різні варіанти мали різні записи кешу
Vary: "Accept-Language, Accept-Encoding",
// CDN-специфічний тег кешу для цілеспрямованого інвалідування
"Cache-Tag": `products,category-${category}`,
},
});
}Патерн stale-while-revalidate особливо потужний на edge. Ось що відбувається:
- Перший запит: Edge отримує від origin, кешує відповідь, повертає її
- Запити протягом 60 секунд: Edge обслуговує з кешу (0 мс затримки origin)
- Запит на 61-360 секунді: Edge негайно обслуговує застарілу кешовану версію, але отримує свіжу версію від origin у фоні
- Після 360 секунд: Кеш повністю прострочений, наступний запит йде до origin
Ваші користувачі майже завжди отримують кешовану відповідь. Компроміс свіжості є явним і налаштовуваним.
Edge Config для динамічної конфігурації#
Edge Config від Vercel (та аналогічні сервіси інших платформ) дозволяє зберігати конфігурацію типу ключ-значення, що реплікується на кожну edge-локацію. Це неймовірно корисно для прапорів функцій, правил редиректів та конфігурації A/B тестів, які ви хочете оновлювати без передеплою:
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
// Читання Edge Config надзвичайно швидке (~1 мс), бо
// дані реплікуються на кожну edge-локацію
const maintenanceMode = await get<boolean>("maintenance_mode");
if (maintenanceMode) {
return NextResponse.rewrite(new URL("/maintenance", request.url));
}
// Прапори функцій
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));
}
// Динамічні редиректи (оновлюйте без передеплою)
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();
}Це справжній game-changer. До Edge Config зміна прапора функції означала зміну коду та передеплой. Тепер ви оновлюєте JSON-значення в дашборді, і воно поширюється глобально за секунди.
Реальна математика продуктивності#
Зробімо чесну математику замість маркетингової. Порівняю три архітектури для типового API-ендпоінту, якому потрібно запитати базу даних:
Сценарій: API профілю користувача (2 запити до бази)#
Архітектура A: Традиційний регіональний Node.js
Користувач у Токіо → Origin у Вірджинії: 140 мс
+ Запит до БД 1 (той самий регіон): 2 мс
+ Запит до БД 2 (той самий регіон): 2 мс
+ Обробка: 5 мс
= Разом: ~149 мс
Архітектура B: Edge-функція з HTTP-базою
Користувач у Токіо → Edge у Токіо: 5 мс
+ Запит до БД 1 (HTTP до Вірджинії): 145 мс
+ Запит до БД 2 (HTTP до Вірджинії): 145 мс
+ Обробка: 3 мс
= Разом: ~298 мс ← ПОВІЛЬНІШЕ ніж регіональний
Архітектура C: Edge-функція з регіональною базою (read replica)
Користувач у Токіо → Edge у Токіо: 5 мс
+ Запит до БД 1 (HTTP до репліки в Токіо): 8 мс
+ Запит до БД 2 (HTTP до репліки в Токіо): 8 мс
+ Обробка: 3 мс
= Разом: ~24 мс ← Найшвидше, але потребує мультирегіональної БД
Архітектура D: Edge для автентифікації + регіональний для даних
Користувач у Токіо → Edge middleware у Токіо: 5 мс (верифікація JWT)
→ Origin у Вірджинії: 140 мс
+ Запит до БД 1 (той самий регіон): 2 мс
+ Запит до БД 2 (той самий регіон): 2 мс
+ Обробка: 5 мс
= Разом: ~154 мс
(Але автентифікація вже верифікована — origin не потрібно перевіряти знову)
(І неавторизовані запити блокуються на edge — ніколи не досягають origin)
Висновки:
- Edge + origin база = часто повільніше, ніж просто використання регіонального сервера
- Edge + мультирегіональна база = найшвидше, але найдорожче і найскладніше
- Edge для привратництва + регіональний для даних = найкращий прагматичний баланс
- Чистий edge (без бази) = неперевершений для таких речей як редиректи та перевірка автентифікації
Архітектура D — це те, що я використовую для більшості проєктів. Edge обробляє те, що йому добре вдається (швидкі рішення, автентифікація, маршрутизація), а регіональний Node.js-сервер обробляє те, що вдається йому (запити до бази, важкі обчислення).
Коли Edge справді перемагає: операції без бази даних#
Математика повністю перевертається, коли немає бази даних:
Редирект (edge):
Користувач у Токіо → Edge у Токіо → відповідь-редирект: ~5 мс
Редирект (регіональний):
Користувач у Токіо → Origin у Вірджинії → відповідь-редирект: ~280 мс
Статична API-відповідь (edge + кеш):
Користувач у Токіо → Edge у Токіо → кешована відповідь: ~5 мс
Статична API-відповідь (регіональний):
Користувач у Токіо → Origin у Вірджинії → відповідь: ~280 мс
Блокування ботів (edge):
Поганий бот будь-де → Edge (найближчий) → 403 відповідь: ~5 мс
(Бот ніколи не досягає вашого origin-сервера)
Блокування ботів (регіональний):
Поганий бот будь-де → Origin у Вірджинії → 403 відповідь: ~280 мс
(Бот все одно споживав ресурси origin)
Для операцій, що не потребують бази даних, edge у 20-50 разів швидший. Це не маркетинг — це фізика.
Мій фреймворк прийняття рішень#
Після року роботи з edge-функціями в продакшені, ось діаграма рішень, яку я використовую для кожного нового ендпоінту або частини логіки:
Крок 1: Чи потрібні Node.js API?#
Якщо імпортує fs, net, child_process або будь-який нативний модуль — Node.js регіональний. Без дебатів.
Крок 2: Чи потрібні запити до бази даних?#
Якщо так, і у вас немає read replica поблизу користувачів — Node.js регіональний (у тому самому регіоні, що й ваша база). Round-trip до бази домінуватиме.
Якщо так, і у вас є глобально розподілені read replica — Edge може працювати, використовуючи HTTP-based клієнти бази даних.
Крок 3: Це рішення щодо запиту (маршрутизація, автентифікація, редирект)?#
Якщо так — Edge. Це sweet spot. Ви приймаєте швидке рішення, що визначає, що станеться із запитом до того, як він досягне origin.
Крок 4: Чи можна кешувати відповідь?#
Якщо так — Edge з правильними заголовками Cache-Control. Навіть якщо перший запит йде до origin, наступні запити обслуговуються з edge-кешу.
Крок 5: Чи CPU-інтенсивно це?#
Якщо включає значні обчислення (обробка зображень, генерація PDF, великі трансформації даних) — Node.js регіональний.
Крок 6: Наскільки це чутливе до затримки?#
Якщо це фонове завдання або webhook — Node.js регіональний. Ніхто його не чекає. Якщо це запит від користувача, де важлива кожна мілісекунда — Edge, якщо відповідає іншим критеріям.
Шпаргалка#
// ✅ ІДЕАЛЬНО для edge
// - Middleware (автентифікація, редиректи, переписування, заголовки)
// - Логіка геолокації
// - Призначення A/B тестів
// - Виявлення ботів / правила WAF
// - API-відповіді, дружні до кешу
// - Перевірка прапорів функцій
// - CORS preflight відповіді
// - Статичні трансформації даних (без БД)
// - Верифікація підписів вебхуків
// ❌ ЗАЛИШИТИ на Node.js регіональному
// - CRUD-операції з базою
// - Завантаження / обробка файлів
// - Маніпуляції з зображеннями
// - Генерація PDF
// - Надсилання email (використовуйте HTTP API, але все ж регіональний)
// - WebSocket-сервери
// - Фонові задачі / черги
// - Все, що використовує нативні npm-пакети
// - SSR-сторінки з запитами до бази
// - GraphQL-резолвери, що звертаються до баз
// 🤔 ЗАЛЕЖИТЬ ВІД СИТУАЦІЇ
// - Автентифікація (edge для JWT, регіональний для session-БД)
// - API-маршрути (edge якщо без БД, регіональний якщо з БД)
// - Серверно-рендерені сторінки (edge якщо дані з кешу/fetch, регіональний якщо з БД)
// - Real-time функції (edge для початкової автентифікації, регіональний для постійних з'єднань)Що я фактично запускаю на Edge#
Для цього сайту ось розподіл:
Edge (middleware):
- Визначення локалі та редирект
- Фільтрація ботів
- Заголовки безпеки (CSP, HSTS тощо)
- Логування доступу
- Обмеження частоти запитів (базове)
Node.js регіональний:
- Рендеринг контенту блогу (обробка MDX потребує Node.js API через Velite)
- API-маршрути, що працюють з Redis
- Генерація OG-зображень (потребує більше часу CPU)
- Генерація RSS-стрічки
Статичний (без runtime взагалі):
- Сторінки інструментів (попередньо рендеряться при збірці)
- Сторінки блог-постів (попередньо рендеряться при збірці)
- Усі зображення та ресурси (обслуговуються CDN)
Найкращий runtime — часто відсутність runtime взагалі. Якщо ви можете попередньо рендерити щось при збірці та обслуговувати як статичний ресурс, це завжди буде швидше за будь-яку edge-функцію. Edge — для речей, які справді повинні бути динамічними при кожному запиті.
Чесне підсумування#
Edge-функції не є заміною традиційних серверів. Вони — доповнення. Вони — додатковий інструмент у вашому архітектурному набору, який неймовірно потужний для правильних випадків використання та активно шкідливий для неправильних.
Евристика, до якої я постійно повертаюся: якщо вашій функції потрібно звертатися до бази даних в одному регіоні, розміщення функції на edge не допомагає — це шкодить. Ви просто додали ще один хоп. Функція запускається швидше, але потім витрачає 100+ мс на зворотній шлях до бази. Чистий результат: повільніше, ніж виконання всього в одному регіоні.
Але для рішень, які можна прийняти лише на основі інформації в самому запиті — геолокація, cookies, заголовки, JWT — edge неперевершений. Ці 5 мс edge-відповіді — не синтетичні бенчмарки. Вони реальні, і ваші користувачі відчувають різницю.
Не переносьте все на edge. Не тримайте все подалі від edge. Розміщуйте кожну частину логіки там, де фізика на її боці.