Il Server-Side Rendering tradizionale su origin server significa latency di 200-400ms in media. Se cachhi l'HTML su un edge CDN, il tempo scende a 20-50ms ma perdi la personalizzazione. L'Edge SSR infrange questo compromesso: ottieni sia la personalizzazione che una risposta sotto i 40ms. Lo realizzi combinando edge runtime come Cloudflare Workers e Vercel Edge con KV store distribuiti. Non ti chiedi più "cache o personalizzazione?" — ottieni entrambi.

Perché Edge SSR è critico oggi

A partire dal 2025, il metrica INP di Chrome è entrata in Core Web Vitals. Una risposta del server superiore a 200ms è sufficiente a infrangere l'INP da sola. Ogni richiesta all'origin aggiunge 150-300ms perché c'è distanza fisica più cold start. L'edge runtime elimina questo ostacolo: il codice gira sul POP (Point of Presence) più vicino all'utente, e i dati vengono recuperati dallo store KV della stessa regione in 5-15ms.

Non è solo velocità. Per la personalizzazione non devi più contattare l'origin. Mantieni i dati come segmento utente, preferenze, stato del carrello nello store KV dell'edge. Quando arriva una richiesta, la funzione edge recupera questi dati e renderizza l'HTML al volo. Il server origin è usato solo per operazioni di scrittura e calcoli pesanti.

Quando lavori con piattaforme come Shopify, questa architettura è particolarmente preziosa. I template Liquid vengono renderizzati su origin, richiedono 300-600ms per pagina. Con Edge SSR componi l'HTML: una funzione edge renderizza la scheda prodotto, un'altra inietta le informazioni del carrello. La latency totale scende sotto i 40ms. Per un'integrazione più dettagliata, puoi consultare l'architettura Headless Commerce.

Cloudflare Workers + KV: il nucleo dell'architettura

Cloudflare Workers funziona basandosi su V8 isolate. Non avvia un nuovo container per ogni richiesta, ma apre un isolate JavaScript. Questo ha un costo di 0.5-2ms. Il codice Worker somiglia a questo:

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const userId = request.headers.get('CF-Connecting-IP') || 'anonymous';
    
    // Recupera il segmento dell'utente da KV
    const segment = await env.USER_SEGMENTS.get(userId);
    
    // Renderizza l'elenco di prodotti in base al segmento
    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 si replica su oltre 300 POP. La latency di lettura è in media 12ms a livello globale. La scrittura si propaga con eventual consistency in 60 secondi. Per questo motivo scrivi su KV solo dati che cambiano di rado: preferenze utente, mappature di segmenti, feature flag. I dati che cambiano frequentemente, come il prezzo del prodotto, li recuperi dall'API origin e li cachhi sull'edge (con Cache API a 60 secondi TTL).

Vercel Edge vs Cloudflare Workers

Vercel Edge Functions usa lo stesso modello V8 isolate ma ha una rete diversa. Cloudflare ha 300+ POP, Vercel ha ~15 edge location regionali. Confronto di latency (utente in Europa, origin negli USA):

RuntimeCold StartKV ReadTTFB Totale
Origin SSR150msN/A380ms
Vercel Edge8ms22ms45ms
Cloudflare Workers1ms11ms28ms

Il vantaggio di Vercel è l'integrazione profonda con l'ecosistema Next.js. Scrivi una funzione edge nel file middleware.ts e fai push in produzione, l'orchestrazione è gestita da Vercel. Su Cloudflare devi usare Wrangler CLI e fare il binding manuale al KV. Compromesso: più controllo vs onboarding più veloce.

Architettura KV store: pattern di scrittura e revalidation

L'eventual consistency di KV edge è un vincolo. Se un utente clicca un pulsante e la preferenza cambia, questo cambiamento si propaga a tutti gli edge in 60 secondi. Durante questo intervallo, diversi POP potrebbero leggere valori diversi. La soluzione: fai un reindirizzamento dall'origin dopo la scrittura oppure implementa un update ottimistico lato client.

Esempio di flusso:

  1. L'utente clicca il toggle "Dark Mode"
  2. Il client invia POST a /api/preferences endpoint (origin)
  3. L'origin scrive user:123:theme = dark su KV
  4. L'origin invoca l'API Cloudflare per invalidare la cache:
// Su 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. La funzione edge legge il nuovo valore da KV nella richiesta successiva
  2. Il JavaScript lato client esegue un soft reload dopo 200ms

Questo pattern limita il throughput di scrittura (il limite di write rate di KV è 1000/secondo per account) ma il throughput di lettura è illimitato. L'architettura è quindi ottimizzata per workload read-heavy. Le azioni degli utenti sono rare (1-2 al minuto), le visualizzazioni di pagina sono frequenti (100+ al secondo).

Strategia di layering della cache

KV non è l'unico layer di cache. Lo stack completo:

Browser Cache (service worker)
  ↓
CDN Edge Cache (Cache API, 60s TTL)
  ↓
Edge KV (eventual, minuti)
  ↓
Database origin

Gli asset statici (CSS, JS) sono in alto, i dati specifici dell'utente in basso. L'HTML stesso è nel middle layer: la funzione edge combina KV + Cache API per renderizzare. Pseudocodice:

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;

Questa architettura tiene il TTFB al 95º percentile sotto i 40ms perché la maggior parte delle richieste viene servita da Cache API (5-8ms). Il tasso di hit di KV è >98%, il fallback all'origin è <2%.

Scope della personalizzazione e compromesso sulla dimensione del bundle

La funzione edge ha un limite di 1MB sulla dimensione del bundle. Non puoi renderizzare componenti React pesanti. Due strategie:

1. Templating minimalista: Usa Handlebars o interpolazione di stringhe personalizzata. Inietta solo variabili:

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');
}

Dimensione del bundle: 2KB. Tempo di rendering: 0.3ms.

2. Partial hydration: Renderizza HTML skeleton sull'edge, hidratta le island React lato client. La funzione 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>
  `;
}

Lato client 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'));

Questo pattern mantiene bassa la latency di Edge SSR (40ms), l'interattività arriva lato client (FCP + 150ms). Compromesso: l'INP potrebbe aumentare (tempo di parse JavaScript). Il monitoraggio è essenziale.

Real user monitoring e alerting

Non puoi ottimizzare la latency dell'edge senza RUM. Cloudflare Analytics aggiunge un header server-timing per ogni richiesta:

Server-Timing: cf-edge;dur=12, cf-kv;dur=8, cf-render;dur=18

Raccogli questo con un PerformanceObserver lato client:

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'] });

Metriche obiettivo:

  • p50 TTFB < 30ms
  • p95 TTFB < 60ms
  • p99 TTFB < 100ms
  • Edge error rate < 0.1%

Se il TTFB supera i 60ms, registra l'ID di traccia di Cloudflare e debugga con Wrangler tail. Di solito il problema è un timeout di KV o un fallback all'origin.

Checklist per il deploy in produzione

Prima di mettere Edge SSR in produzione:

  1. Rate limiting: Throttle le write di KV (1 write per utente al secondo)
  2. Fallback chain: Se KV va in timeout (>50ms), fallback all'origin; se origin va in timeout, servi HTML statico
  3. Feature flag: Rendi progressiva la personalizzazione dell'edge (%10 → %50 → %100 del traffico)
  4. Cost monitoring: Cloudflare Workers offre 100K richieste/giorno gratuitamente, poi $0.50/milione. Le letture di KV sono gratuite illimitatamente, le write sono $0.50/milione.
  5. Security: Esegui l'hash dell'ID utente, non memorizzare PII nella chiave KV, aggiungi bot detection per bypass dei limiti di rate

Proiezione dei costi: 1M visite al giorno, %30 richieste personalizzate = 300K edge invocation/giorno = $0.15/giorno = $4.50/mese. L'alternativa Origin SSR: istanza 2 vCPU a $50/mese. Risparmi: 91%.

Una volta configurata l'architettura Edge SSR, il costo incrementale è zero. Aggiungere una nuova regola di personalizzazione significa solo scrivere una nuova chiave su KV. Creare un nuovo segmento significa aggiungere un blocco if nella funzione edge. Lo scaling non è lineare ma logaritmico — 10M richieste/giorno vengono servite con la stessa latency di 40ms. Per questo, pensare edge-first nella strategia di crescita offre un vantaggio sostanziale.