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):
| Runtime | Cold Start | KV Read | TTFB Totale |
|---|---|---|---|
| Origin SSR | 150ms | N/A | 380ms |
| Vercel Edge | 8ms | 22ms | 45ms |
| Cloudflare Workers | 1ms | 11ms | 28ms |
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:
- L'utente clicca il toggle "Dark Mode"
- Il client invia POST a
/api/preferencesendpoint (origin) - L'origin scrive
user:123:theme = darksu KV - 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}`] })
});
- La funzione edge legge il nuovo valore da KV nella richiesta successiva
- 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:
- Rate limiting: Throttle le write di KV (1 write per utente al secondo)
- Fallback chain: Se KV va in timeout (>50ms), fallback all'origin; se origin va in timeout, servi HTML statico
- Feature flag: Rendi progressiva la personalizzazione dell'edge (%10 → %50 → %100 del traffico)
- 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.
- 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.