Традиционный серверный рендеринг на origin-серверах означает латентность 200–400ms в среднем. Если кэшировать HTML на CDN edge, это сократится до 20–50ms, но персонализация потеряется. Edge SSR разрывает эту дилемму: получаешь одновременно и персонализацию, и ответ менее чем за 40ms. Достигается это через edge runtime'ы вроде Cloudflare Workers и Vercel Edge в сочетании с распределённым хранилищем KV. Больше не нужно выбирать между кэшем и персонализацией — берёшь и то, и другое.
Почему Edge SSR критичен сейчас
С 2025 года метрика INP браузера Chrome вошла в Core Web Vitals. Ответ сервера более 200ms сам по себе способен разрушить INP. Каждый запрос к origin добавляет 150–300ms из-за физического расстояния и cold start. Edge runtime убирает эту преграду: код выполняется в ближайшем к пользователю POP'е (Point of Presence), данные из KV в том же регионе приходят за 5–15ms.
Это не только скорость. Персонализация теперь не требует запроса к origin. Сегмент пользователя, предпочтения, статус корзины хранятся в edge KV. Когда приходит запрос, функция на edge извлекает эти данные и тут же рендерит HTML. Origin-сервер используется только для операций записи и сложных вычислений.
При работе с платформами вроде Shopify эта архитектура особенно важна. Шаблоны Liquid рендерятся на origin и отнимают 300–600ms на страницу. С Edge SSR HTML становится composable: одна edge-функция рендерит карточку товара, другая инжектирует информацию о корзине. Общая латентность падает ниже 40ms. Подробнее об интеграции см. Headless Commerce.
Cloudflare Workers + KV: ядро архитектуры
Cloudflare Workers работает на основе V8 isolate. Для каждого запроса не создаётся новый контейнер, открывается JavaScript isolate. Это обходится в 0.5–2ms. Код Worker выглядит так:
export default {
async fetch(request, env) {
const url = new URL(request.url);
const userId = request.headers.get('CF-Connecting-IP') || 'anonymous';
// Извлечь сегмент пользователя из KV
const segment = await env.USER_SEGMENTS.get(userId);
// Рендерить список товаров в зависимости от сегмента
const products = segment === 'premium'
? await fetchPremiumProducts()
: await fetchStandardProducts();
const html = renderHTML(products, segment);
return new Response(html, {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}
};
Cloudflare KV реплицируется на 300+ POP'ах. Средняя латентность чтения — 12ms глобально. Запись распространяется через eventual consistency за 60 секунд. Поэтому в KV пишутся только редко изменяющиеся данные: настройки пользователей, маппинги сегментов, флаги функций. Часто изменяемые данные, например цены товаров, берутся из origin API и кэшируются на edge (Cache API с TTL 60 секунд).
Vercel Edge vs Cloudflare Workers
Vercel Edge Functions тоже использует V8 isolate, но сеть другая. Cloudflare имеет 300+ POP'ов, Vercel — около 15 региональных edge-локаций. Сравнение латентности (пользователь в Европе, origin в США):
| Runtime | Cold Start | KV Read | Total TTFB |
|---|---|---|---|
| Origin SSR | 150ms | N/A | 380ms |
| Vercel Edge | 8ms | 22ms | 45ms |
| Cloudflare Workers | 1ms | 11ms | 28ms |
Преимущество Vercel — глубокая интеграция с экосистемой Next.js. Пишешь edge-функцию в middleware.ts, запушиваешь в production, оркестрация на стороне Vercel. На Cloudflare нужно самостоятельно использовать Wrangler CLI и вручную связывать KV. Компромисс: больше контроля против более быстрого старта.
Архитектура KV: pattern записи и ревалидация
Eventual consistency KV — ограничение. Пользователь кликнул кнопку, предпочтение изменилось — это распространится на все edge за 60 секунд. В этот период разные POP'ы могут читать разные значения. Решение: после записи перенаправить на origin или выполнить оптимистичное обновление на клиенте.
Пример flow'а:
- Пользователь нажимает переключатель "Тёмный режим"
- Клиент отправляет POST на
/api/preferences(origin) - Origin пишет в KV
user:123:theme = dark - Origin вызывает Cloudflare API для немедленной ревалидации кэша:
// На origin'е
await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiToken}` },
body: JSON.stringify({ files: [`https://example.com/user/${userId}`] })
});
- Edge-функция в следующем запросе читает новое значение из KV
- Клиентский JavaScript через 200ms выполняет мягкую перезагрузку
Этот pattern ограничивает пропускную способность записей (rate limit KV: 1000/сек на аккаунт), но пропускная способность чтения не ограничена. Поэтому архитектура оптимизирована для read-heavy нагрузок. Действия пользователя редки (1–2 раза в минуту), просмотры страниц часты (100+ в секунду).
Стратегия многоуровневого кэширования
KV — не единственный уровень кэша. Полный стек:
Кэш браузера (service worker)
↓
Кэш CDN Edge (Cache API, 60s TTL)
↓
Edge KV (eventual, минуты)
↓
База данных Origin
Статические ресурсы (CSS, JS) на верхних уровнях, данные пользователя на нижних. Сам HTML находится в middle layer: edge-функция комбинирует KV и Cache API для рендеринга. Псевдокод:
const cacheKey = `html:${url}:${segment}`;
let html = await caches.default.match(cacheKey);
if (!html) {
const userData = await KV.get(userId);
html = renderTemplate(userData);
await caches.default.put(cacheKey, html, { expirationTtl: 60 });
}
return html;
Эта конструкция держит TTFB на 95-м перцентиле ниже 40ms, потому что большинство запросов обслуживаются из Cache API (5–8ms). Hit rate KV выше 98%, fallback на origin менее 2%.
Scope персонализации и trade-off размера бандла
Edge-функция имеет лимит размера бандла 1MB (Cloudflare). Нельзя рендерить тяжёлые React-компоненты. Два подхода:
1. Минимальный templating: используй Handlebars или кастомную интерполяцию строк. Просто инжектируй переменные:
const template = `<div class="product-card">
<h3>{{name}}</h3>
<span class="price {{priceClass}}">{{price}}</span>
</div>`;
function render(product, segment) {
return template
.replace('{{name}}', product.name)
.replace('{{price}}', segment === 'premium' ? product.premiumPrice : product.price)
.replace('{{priceClass}}', segment === 'premium' ? 'gold' : 'standard');
}
Размер бандла: 2KB. Время рендеринга: 0.3ms.
2. Partial hydration: рендерируй skeleton HTML на edge, гидрируй React-island'ы на клиенте. Edge-функция:
export default async function(request) {
const products = await fetchProducts();
return `
<div id="product-list" data-products='${JSON.stringify(products)}'>
${products.map(p => `<div class="skeleton"></div>`).join('')}
</div>
<script type="module" src="/hydrate.js"></script>
`;
}
Клиентский код hydrate.js (10KB):
import { h, render } from 'preact';
const data = JSON.parse(document.getElementById('product-list').dataset.products);
render(<ProductList products={data} />, document.getElementById('product-list'));
Этот паттерн держит edge SSR латентность низкой (40ms), интерактивность приходит с клиента (FCP + 150ms). Trade-off: INP может вырасти (время парсинга JavaScript). Требуется мониторинг.
Real User Monitoring и alerting
Без RUM невозможно оптимизировать edge-латентность. Cloudflare Analytics добавляет Server-Timing header в каждый запрос:
Server-Timing: cf-edge;dur=12, cf-kv;dur=8, cf-render;dur=18
Собирай это на клиенте через PerformanceObserver:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'navigation') {
const ttfb = entry.responseStart - entry.requestStart;
fetch('/analytics', {
method: 'POST',
body: JSON.stringify({ ttfb, url: entry.name })
});
}
}
}).observe({ entryTypes: ['navigation'] });
Целевые метрики:
- p50 TTFB < 30ms
- p95 TTFB < 60ms
- p99 TTFB < 100ms
- Error rate на edge < 0.1%
Запросы, превышающие 60ms, логируй с trace ID от Cloudflare, дебагируй через wrangler tail. Чаще всего причина — timeout KV или fallback на origin.
Чек-лист для production-deployment'а
Перед запуском Edge SSR в production:
- Rate limiting: throttle KV write (максимум 1 запись в секунду на пользователя)
- Fallback chain: если KV timeout (>50ms), fallback на origin; если origin timeout, отправь static HTML
- Feature flag: раскатывай edge-персонализацию постепенно (10% → 50% → 100% трафика)
- Cost monitoring: Cloudflare Workers даёт 100K запросов/день бесплатно, далее $0.50 за миллион. KV read бесплатен без лимита, write стоит $0.50 за миллион.
- Security: хэшируй user ID, не храни PII в ключах KV, добавь bot detection для защиты от rate limit bypass
Расчёт стоимости: 1M посещений в день, 30% персонализированных запросов = 300K edge invocation/день = $0.15/день = $4.50/месяц. Альтернатива (origin SSR): инстанс с 2 vCPU стоит $50/месяц. Экономия: 91%.
После настройки Edge SSR incremental cost стремится к нулю. Добавить новое правило персонализации — просто записать новый ключ в KV. Создать новый сегмент — добавить один if-блок в edge-функцию. Масштабирование нелинейное — 10M запросов в день обслуживаются с той же 40ms латентностью. Вот почему в стратегии роста важно думать edge-first с самого начала.