В традиционных SSR архитектурах latency персонализации держится в диапазоне 200–400ms. Когда нужно отрендерить страницу с учётом локации пользователя, его предпочтений и истории поведения, это время может вырасти до 600ms. Edge SSR позволяет снизить эту цифру до 40ms — однако если архитектура не продумана должным образом, ограничения edge окружения (лимит CPU, cold start, объём памяти) уничтожат производительность. В этой статье разбираем анатомию production архитектуры на Cloudflare Workers + KV: какие данные хранить на edge, какие запросы направлять в origin и какие tradeoff'ы необходимы для гарантии 40ms latency.
Отличие Edge SSR от классического Origin SSR
В классическом SSR запрос следует по цепочке: CDN → origin server → database → render → response. Каждый hop добавляет 20–60ms latency, итого 250–400ms. Edge SSR разрывает эту цепь: запрос падает в edge runtime (Cloudflare Workers или Vercel Edge Function), чтение из KV store занимает 5–15ms, рендер завершается за 10–25ms. Общая latency падает до 40–60ms.
Разница не только в географической близости — архитектура принципиально иная. Edge runtime используют V8 isolate технологию, cold start составляет 0–5ms. Node.js контейнер холодный старт может занять 200–800ms. Распределённая key-value структура KV store устраняет latency overhead TCP handshake к database. Пример: запрос в Postgres для получения сегментации пользователей занимает 80–120ms (connection + query + parsing), то же данные из Cloudflare KV читаются за 8–12ms.
Tradeoff таков: edge runtime имеет лимит CPU 50ms, limit памяти около 128MB (зависит от платформы). Если выполнять тяжёлые вычисления или большой JSON parsing, превысите лимит. Поэтому на edge рендерится только "горячий путь" — сложные операции остаются в origin.
Анатомия архитектуры KV Store
Не думайте о KV store как о cache — проектируйте его как о распределённом global state. Вот структура, которую мы используем: каждый сегмент пользователя (например "premium-tr", "free-us") становится namespace ключом, value — JSON с правилами персонализации. Формат ключа: user_segment:{segment_id}:config. Этот config содержит правила: какой hero image показывать, какой текст в price note, как изменяется CTA.
// Пример для Cloudflare Workers
interface UserSegmentConfig {
heroImage: string;
ctaText: string;
priceNote: string;
featureFlags: string[];
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const segmentId = getCookie(request, 'segment_id') || 'default';
const configKey = `user_segment:${segmentId}:config`;
const configRaw = await env.KV_NAMESPACE.get(configKey);
if (!configRaw) {
// Fallback: получить из origin, записать в KV
const originConfig = await fetchFromOrigin(segmentId);
await env.KV_NAMESPACE.put(configKey, JSON.stringify(originConfig), {
expirationTtl: 3600 // 1 час
});
return renderPage(originConfig);
}
const config: UserSegmentConfig = JSON.parse(configRaw);
return renderPage(config);
}
};
Функция renderPage выполняет inline HTML string interpolation на edge — template engine не используем, потому что bundle size может упереться в лимит 128MB. Вместо этого используем литеральные строки или лёгкий JSX-to-string трансформер.
KV TTL стратегия критична: с TTL 1 час мы refresh'имся из origin раз в час. Если контент меняется часто (например flash sale), TTL можно снизить до 5 минут, но это повысит origin hit rate на 15–20%. В нашем сценарии конфиг сегмента меняется 2–3 раза в день, 1 час — идеальный balance point.
KV Write Стратегия: Cache-Aside vs Write-Through
Две стратегии: cache-aside (как в примере выше — при miss берём из origin, пишем в KV) и write-through (при update origin webhook'ом инвалидируем KV или пишем напрямую). Мы используем cache-aside, потому что webhook latency добавляет 2–3% failure rate (network timeout, retry logic). При cache-aside первый запрос медленнее (200ms), все последующие завершаются за 40ms. На 1M pageview/день overhead первого запроса незначителен.
Если выбираете write-through, используйте Cloudflare Queue API или Vercel ISR подобный механизм — webhook не должен писать напрямую в KV, а должен push'ить в queue, worker'а consume'ить из queue и писать в KV. Это даёт retry гарантию и rate limiting.
Vercel Edge vs Cloudflare Workers: Критерии выбора архитектуры
Две платформы похожи, но имеют значимые отличия. Cloudflare Workers имеет native KV, глобальная репликация автоматическая, pricing благоприятнее для read-heavy workload ($0.50 за 10M read против Vercel Edge Redis-like pricing). Vercel Edge лучше интегрирован с Next.js, TypeScript DX сильнее, но в качестве KV альтернативы используется Vercel KV (Upstash Redis базированный) — это добавляет дополнительную latency (12–18ms против 5–10ms Cloudflare KV).
Мы на Cloudflare Workers предпочитаем для Headless e-commerce проектов, потому что трафик read-heavy (страницы товаров, категории читаются постоянно, запись редка). Vercel Edge используем в Next.js App Router проектах как middleware — потому что API route'ы и server component'ы остаются в том же репо, deployment pipeline един.
Benchmark: запустили ту же logic персонализации на обеих платформах. Cloudflare Workers P95 latency 42ms, Vercel Edge P95 latency 58ms (из-за Vercel KV overhead). CPU использование похожее (15–20ms), разница в storage read latency.
Оптимизация Cold Start и Bundle Size
Хотя edge runtime'ы имеют низкий cold start, большой bundle size создаёт проблемы. Cloudflare Workers имеет лимит 1MB на script размер (compressed), Vercel Edge принимает ~1MB bundle но с ростом cold start увеличивается. Вот тактики которые мы применяем:
1. Pruning зависимостей: lodash → lodash-es (tree-shakeable), moment → date-fns. С analyzer'ом bundle'а удалили неиспользуемые модули — с 340KB до 180KB.
2. Запрет динамического import'а: На edge динамический import() увеличивает cold start на 30–50ms. Все зависимости импортируйте статично, дайте bundler'у возможность делать tree-shaking.
3. Inline критичного кода: Если logic персонализации это 40–50 строк — пишите inline вместо отдельного модуля. Module resolution добавляет даже 2–3ms.
// ❌ Плохо: отдельный модуль
import { renderHero } from './heroRenderer';
// ✅ Хорошо: inline
function renderHero(config: UserSegmentConfig): string {
return `<div class="hero">${config.heroImage}</div>`;
}
4. Wasm использование: Если нужны тяжёлые операции (JSON schema валидация, markdown parsing) — пишите на Rust или Go, скомпилируйте в Wasm. Wasm модуль будет 50–80KB, экономия JavaScript bundle'а 200–300KB. Однако Wasm instantiation добавляет 10–15ms — взвесьте tradeoff.
Monitoring и гарантия latency
Для гарантии 40ms latency target'а устанавливаем RUM и synthetic monitoring. Cloudflare Workers Analytics API предоставляет P50/P95/P99 latency метрики, отправляем их в Grafana. Alarm threshold: если P95 > 60ms — alert.
// Пример Analytics Event для Workers
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const startTime = Date.now();
const response = await handleRequest(request, env);
const duration = Date.now() - startTime;
ctx.waitUntil(
env.ANALYTICS.writeDataPoint({
blobs: [request.url],
doubles: [duration],
indexes: [request.headers.get('cf-ray') || '']
})
);
return response;
}
};
ctx.waitUntil выполняет асинхронную запись аналитики не добавляя к response latency — критично. Если использовать await, каждый запрос получит +5–10ms.
Для synthetic monitoring используем Checkly или Pingdom — 5 географических локаций, 1 запрос в минуту, latency > 70ms → Slack alert. Так мы детектируем edge node деградацию за 3–5 минут.
Origin Fallback и graceful degradation
Не всё можно handle'ить на edge — KV timeout, CPU лимит, неожиданная ошибка. В таких случаях нужен fallback на origin. Вот стратегия которую мы выбрали: если edge error rate > 1% в течение 10 минут, весь трафик направляется в origin на 10 минут, затем возвращаемся на edge.
async function handleWithFallback(request: Request, env: Env): Promise<Response> {
try {
const edgeResponse = await renderEdge(request, env);
return edgeResponse;
} catch (error) {
// Log to Sentry/Datadog
console.error('Edge render failed:', error);
// Proxy в origin
return fetch(request.url, {
headers: request.headers,
cf: { cacheEverything: true }
});
}
}
Этот fallback механизм даёт %99.8 uptime. Когда edge fails, latency растёт до 200–250ms (origin SSR), но user experience сохраняется. Альтернатива: возвращать статический fallback HTML при edge ошибке — но это недопустимо в e-commerce (потеря персонализации = потеря conversions).
Production результаты и сравнение
За 6 месяцев на production с 12M pageview видели такие числа: P50 latency 38ms, P95 latency 54ms, P99 latency 89ms (P99 где origin fallback активируется). Сравнение с origin SSR: P50 220ms → 38ms (83% снижение), P95 380ms → 54ms (86% снижение).
Core Web Vitals эффект: LCP 2.4s → 1.1s (hero image персонализация рендерится на edge), FCP 1.8s → 0.9s, TBT не изменилось (JavaScript bundle одинаков). Conversion rate выросла на 2.8% (A/B test, 95% confidence) — latency снижение напрямую отразилось на business метриках.
Стоимость: Cloudflare Workers + KV $180/месяц (10M request, 50M KV read), origin SSR EC2 instance стоил $420. 57% снижение стоимости + 86% снижение latency. ROI расчёт: development effort 120 часов (2 week sprint), payback period 2 месяца.
Edge SSR архитектура не magic bullet — без правильного data modeling'а, KV стратегии и fallback механизма потерпит неудачу. Но когда эти три компонента продуманы правильно, 40ms latency становится гарантируемым target'ом.