Edge Runtime в Next.js
Next.js позволяет запускать код не только на классическом Node.js, но и в легковесной среде Edge Runtime, которая работает ближе к пользователю на CDN-узлах. Эта возможность особенно ценна для middleware, простых API-обработчиков и задач, где критична низкая задержка. В статье разберём архитектуру Edge Runtime, его ограничения и практические примеры использования.
Содержание
- Что такое Edge Runtime
- Как работает Edge Runtime
- Когда использовать Edge Runtime
- Ограничения Edge Runtime
- Конфигурация
- Практические примеры
- Edge vs Serverless
- Оптимизация Edge функций
- Deployment
- Лучшие практики
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Что такое Edge Runtime
Edge Runtime — это среда выполнения JavaScript, оптимизированная для низкой задержки (latency) и быстрого запуска. В отличие от традиционного Node.js сервера, Edge функции выполняются на “edge” — ближе к пользователю, на CDN-серверах по всему миру.
Ключевые отличия от Node.js
Node.js Runtime:
- Полноценный серверный runtime
- Доступ ко всей файловой системе
- Поддержка всех Node.js API
- Запускается на одном или нескольких серверах
- Время запуска: ~100-500ms
- Холодный старт: заметная задержка
Edge Runtime:
- Легковесный runtime на основе V8 Isolate
- Ограниченный доступ к API (Web API только)
- Нет файловой системы
- Запускается на CDN по всему миру
- Время запуска: ~5-50ms
- Холодный старт: минимальный
Как работает Edge Runtime
Архитектура
┌─────────────────────────────────────────────────────┐
│ Пользователь │
│ (Москва, Россия) │
└──────────────────────┬──────────────────────────────┘
│
▼
┌──────────────────────────┐
│ Edge Location (CDN) │
│ Москва / Санкт-Петербург│
│ │
│ ┌────────────────────┐ │
│ │ Edge Function │ │
│ │ (V8 Isolate) │ │
│ │ - Быстрый запуск │ │
│ │ - Минимум памяти │ │
│ └────────────────────┘ │
└──────────────────────────┘
V8 Isolate
Edge Runtime использует V8 Isolates — легковесные изолированные среды выполнения JavaScript:
- Быстрый запуск — не нужно загружать весь Node.js runtime
- Минимум памяти — каждый isolate занимает ~1-5MB (vs ~50-100MB для Node.js)
- Параллелизм — тысячи isolates на одном сервере
- Безопасность — изолированы друг от друга
Когда использовать Edge Runtime
Подходит для
1. Middleware
// middleware.ts
export const config = {
matcher: ['/((?!api|_next/static|favicon.ico).*)'],
}
export function middleware(request) {
// Геолокация
const country = request.geo?.country || 'US'
// A/B тестирование
const variant = Math.random() > 0.5 ? 'A' : 'B'
// Аутентификация
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect('/login')
}
return NextResponse.next()
}
2. API Routes с низкой задержкой
// app/api/geo/route.ts
export const runtime = 'edge'
export async function GET(request: Request) {
const country = request.geo?.country
const city = request.geo?.city
return Response.json({ country, city })
}
3. Простые страницы
// app/page.tsx
export const runtime = 'edge'
export default function Page() {
return <h1>Быстрая страница</h1>
}
4. A/B тестирование
export const runtime = 'edge'
export async function GET(request) {
const bucket = Math.random() > 0.5 ? 'control' : 'variant'
return new Response(JSON.stringify({ bucket }), {
headers: {
'Content-Type': 'application/json',
'Set-Cookie': `ab-test=${bucket}; Path=/`,
},
})
}
5. Геолокация и локализация
export const runtime = 'edge'
export async function GET(request) {
const country = request.geo?.country
const locale = country === 'RU' ? 'ru' : 'en'
const content = await getLocalizedContent(locale)
return Response.json(content)
}
НЕ подходит для
1. Тяжелые вычисления
// Плохо — Edge не для этого
export const runtime = 'edge'
export async function POST(request) {
const data = await request.json()
// Тяжелая обработка изображений
const processed = await sharp(data.image)
.resize(1000, 1000)
.toBuffer()
return Response.json({ processed })
}
2. Работа с файловой системой
// Плохо — нет fs в Edge
export const runtime = 'edge'
export async function GET() {
const file = await fs.readFile('./data.json') // Ошибка!
return Response.json(JSON.parse(file))
}
3. Большие зависимости
// Плохо — большие бандлы не поддерживаются
export const runtime = 'edge'
import { PrismaClient } from '@prisma/client' // Слишком большой
export async function GET() {
const prisma = new PrismaClient()
const users = await prisma.user.findMany()
return Response.json(users)
}
4. Долгие операции
// Плохо — Edge имеет лимиты на время выполнения
export const runtime = 'edge'
export async function POST() {
// Генерация видео может занять минуты
await generateVideo() // Timeout!
return Response.json({ done: true })
}
Ограничения Edge Runtime
Поддерживаемые API
Web API (поддерживаются):
fetch— HTTP запросыRequest,Response— работа с HTTPURL,URLSearchParams— парсинг URLHeaders— HTTP заголовкиcrypto— Web Crypto APITextEncoder,TextDecoder— работа с текстомsetTimeout,setInterval— таймеры (с ограничениями)
Node.js API (НЕ поддерживаются):
fs— файловая системаchild_process— дочерние процессыcluster— кластеризацияnet,tls— сетевые сокетыprocess— частично (толькоprocess.env)
Лимиты
Vercel Edge Functions:
- Размер бандла: до 4MB (сжатый)
- Время выполнения: до 30 секунд
- Память: до 128MB
- Количество запросов: зависит от плана
Cloudflare Workers:
- Размер скрипта: до 1MB (free), до 5MB (paid)
- Время выполнения: до 30 секунд (free), до 50ms CPU (paid)
- Память: до 128MB
Конфигурация
Route Handlers
// app/api/data/route.ts
export const runtime = 'edge' // 'nodejs' | 'edge'
export async function GET(request: Request) {
return Response.json({ data: 'Hello from Edge' })
}
Pages
// app/page.tsx
export const runtime = 'edge'
export default function Page() {
return <div>Edge Page</div>
}
Middleware
// middleware.ts
export const config = {
matcher: ['/((?!api|_next/static|favicon.ico).*)'],
}
export function middleware(request) {
return NextResponse.next()
}
Практические примеры
Аутентификация через Middleware
// middleware.ts
import { NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
export async function middleware(request) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET!)
await jwtVerify(token, secret)
return NextResponse.next()
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*'],
}
Геолокация и редирект
// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request) {
const country = request.geo?.country || 'US'
// Редирект для конкретных стран
if (country === 'RU') {
return NextResponse.redirect(new URL('/ru', request.url))
}
if (country === 'CN') {
return NextResponse.redirect(new URL('/zh', request.url))
}
return NextResponse.next()
}
Feature Flags
// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request) {
const userId = request.cookies.get('user-id')?.value
// Простая логика feature flags
const enableNewUI = userId && parseInt(userId) % 10 < 3 // 30% пользователей
const response = NextResponse.next()
response.headers.set('x-feature-new-ui', enableNewUI ? 'true' : 'false')
return response
}
Rate Limiting
// middleware.ts
import { NextResponse } from 'next/server'
const rateLimit = new Map<string, { count: number; resetAt: number }>()
export function middleware(request) {
const ip = request.ip || 'unknown'
const now = Date.now()
let record = rateLimit.get(ip)
if (!record || now > record.resetAt) {
record = { count: 0, resetAt: now + 60000 } // 1 минута
rateLimit.set(ip, record)
}
record.count++
if (record.count > 100) {
return new NextResponse('Too Many Requests', { status: 429 })
}
return NextResponse.next()
}
A/B тестирование
// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request) {
let bucket = request.cookies.get('ab-bucket')?.value
if (!bucket) {
bucket = Math.random() > 0.5 ? 'control' : 'variant'
const response = NextResponse.next()
response.cookies.set('ab-bucket', bucket, {
maxAge: 60 * 60 * 24 * 30, // 30 дней
})
return response
}
return NextResponse.next()
}
Edge vs Serverless
Edge Functions
Плюсы:
- Мгновенный запуск (~5ms)
- Глобальное распределение
- Низкая задержка
- Масштабирование до тысяч запросов
Минусы:
- Ограниченный runtime
- Лимиты на размер и время
- Нет доступа к Node.js API
Serverless Functions (Node.js)
Плюсы:
- Полный Node.js runtime
- Нет ограничений на зависимости
- Доступ ко всем API
Минусы:
- Холодный старт (~100-500ms)
- Задержка от региона
- Ограниченное количество инстансов
Когда что использовать
Edge Runtime:
- Middleware
- Простые API endpoints
- Геолокация
- A/B тестирование
- Аутентификация
- Редиректы
Serverless (Node.js):
- Сложная бизнес-логика
- Работа с БД
- Обработка файлов
- Тяжелые вычисления
- Интеграции с внешними сервисами
Оптимизация Edge функций
Минимизируйте размер бандла
// Плохо
import { PrismaClient } from '@prisma/client'
// Хорошо — используйте HTTP клиент
export const runtime = 'edge'
export async function GET() {
const users = await fetch('https://api.example.com/users').then(r => r.json())
return Response.json(users)
}
Используйте кэширование
export const runtime = 'edge'
export async function GET() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Кэш на 1 час
})
return Response.json(await data.json())
}
Избегайте больших зависимостей
// Вместо moment.js
import { format } from 'date-fns'
// Вместо lodash
import { debounce } from 'lodash-es'
Deployment
Vercel
Edge функции автоматически деплоятся на Vercel Edge Network:
vercel deploy
Cloudflare Workers
npm install @cloudflare/next-on-pages
npx @cloudflare/next-on-pages
Self-hosted
Для self-hosted deployments используйте адаптеры:
@edge-runtime/next-server- Cloudflare Workers
- Deno Deploy
Лучшие практики
- Используйте Edge для middleware — минимальная задержка
- Минимизируйте зависимости — меньше размер бандла
- Кэшируйте ответы — используйте
Cache-Control - Обрабатывайте ошибки — Edge функции могут падать
- Тестируйте локально —
next devподдерживает Edge runtime - Мониторьте метрики — следите за временем выполнения
- Не используйте для сложной логики — Edge не для этого
Ключевые тезисы для интервью
- Edge Runtime в Next.js основан на V8 Isolates и работает на CDN-узлах, обеспечивая запуск за миллисекунды.
- В отличие от Node.js runtime, Edge не имеет доступа к файловой системе и большинству Node.js API, доступны только Web API.
- Edge идеально подходит для middleware, простых API-роутов, геолокации, редиректов, аутентификации и A/B-тестирования.
- Для тяжёлых вычислений, работы с файлами, большими зависимостями и долгих операций лучше оставаться на Node.js runtime.
- Включить Edge можно через
export const runtime = 'edge'в Route Handler или странице; middleware всегда выполняется в Edge. - Edge функции имеют лимиты: размер бандла, память и время выполнения, которые отличаются у разных провайдеров.
- Vercel Edge Functions ограничены 4 MB бандла и 128 MB памяти, Cloudflare Workers — до 1–5 MB скрипта и 128 MB памяти.
- Для оптимизации Edge функций важно минимизировать размер бандла, избегать тяжёлых зависимостей и использовать кэширование.
- Edge Runtime — не замена Node.js, а дополнительный инструмент для сценариев с критичной latency и глобальным распределением.
Заключение
Edge Runtime — мощный инструмент для оптимизации задержки и масштабирования. Используйте его для middleware, простых API, геолокации и A/B тестирования. Для сложной бизнес-логики и работы с БД оставляйте Node.js runtime.
Помните: Edge — это не замена Node.js, а дополнение для специфических задач, где важна низкая задержка и глобальное распределение.