Традиционный серверный рендеринг на 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 в США):

RuntimeCold StartKV ReadTotal TTFB
Origin SSR150msN/A380ms
Vercel Edge8ms22ms45ms
Cloudflare Workers1ms11ms28ms

Преимущество Vercel — глубокая интеграция с экосистемой Next.js. Пишешь edge-функцию в middleware.ts, запушиваешь в production, оркестрация на стороне Vercel. На Cloudflare нужно самостоятельно использовать Wrangler CLI и вручную связывать KV. Компромисс: больше контроля против более быстрого старта.

Архитектура KV: pattern записи и ревалидация

Eventual consistency KV — ограничение. Пользователь кликнул кнопку, предпочтение изменилось — это распространится на все edge за 60 секунд. В этот период разные POP'ы могут читать разные значения. Решение: после записи перенаправить на origin или выполнить оптимистичное обновление на клиенте.

Пример flow'а:

  1. Пользователь нажимает переключатель "Тёмный режим"
  2. Клиент отправляет POST на /api/preferences (origin)
  3. Origin пишет в KV user:123:theme = dark
  4. 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}`] })
});
  1. Edge-функция в следующем запросе читает новое значение из KV
  2. Клиентский 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:

  1. Rate limiting: throttle KV write (максимум 1 запись в секунду на пользователя)
  2. Fallback chain: если KV timeout (>50ms), fallback на origin; если origin timeout, отправь static HTML
  3. Feature flag: раскатывай edge-персонализацию постепенно (10% → 50% → 100% трафика)
  4. Cost monitoring: Cloudflare Workers даёт 100K запросов/день бесплатно, далее $0.50 за миллион. KV read бесплатен без лимита, write стоит $0.50 за миллион.
  5. 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 с самого начала.