
Performance, HTTP/3 ed edge computing: perché la latenza è diventata un vantaggio competitivo
HTTP/3 su QUIC, edge computing e WebAssembly spiegati con esempi pratici: cosa migliorano davvero su mobile, come attivarli e quando non servono.
Per anni “ottimizzare le performance” ha voluto dire soprattutto ridurre il peso: meno JavaScript, immagini compresse, CSS critico inline. È ancora tutto vero, e l’ho raccontato con numeri reali nell’articolo sui Core Web Vitals. Ma su mobile, dove ormai passa la maggior parte del traffico, il collo di bottiglia spesso non è la banda: è la latenza. Ogni andata e ritorno tra telefono e server costa decine o centinaia di millisecondi, e una pagina ne fa tante prima di mostrare qualcosa.
Tre tecnologie attaccano proprio quel problema, ciascuna da un lato diverso:
| Tecnologia | Cosa riduce | Dove agisce |
|---|---|---|
| HTTP/3 + QUIC | Round trip di connessione, blocchi su rete instabile | Trasporto, tra client e server |
| Edge computing | Distanza fisica tra utente e codice | Infrastruttura, dove gira il server |
| WebAssembly | Tempo di calcolo per lavoro CPU-intensivo | Esecuzione, nel browser o all’edge |
In questo articolo vediamo cosa fa davvero ognuna, come attivarla, come misurare se serve e — altrettanto importante — quando non cambia nulla.
Perché i millisecondi sono un tema di business
Il collegamento tra velocità e conversioni non è un’intuizione da sviluppatori. Lo studio Milliseconds Make Millions (Deloitte per Google, 2020) ha misurato che un miglioramento di 0,1 secondi nella velocità di un sito mobile si associava a un aumento delle conversioni dell’8,4% nel retail e del 10,1% nel travel. Sono numeri da prendere come ordine di grandezza, non come promessa, ma la direzione è confermata da quasi ogni case study pubblicato da allora.
La parte meno ovvia è dove si perde quel tempo. Su una connessione 4G il tempo di andata e ritorno (RTT) verso un server è tipicamente tra 50 e 100 ms; su una rete congestionata o in mobilità supera facilmente i 200 ms. Ora conta quanti viaggi servono prima del primo byte utile:
| Fase | HTTP/1.1 o HTTP/2 su TCP + TLS 1.3 | HTTP/3 su QUIC |
|---|---|---|
| Handshake di trasporto | 1 RTT (TCP) | Combinato |
| Handshake crittografico | 1 RTT (TLS 1.3) | 1 RTT in totale |
| Ripresa di una connessione | 1 RTT (TCP) + TLS resumption | 0 RTT possibile |
| Richiesta e risposta | 1 RTT | 1 RTT |
Con un RTT di 100 ms, passare da 2 a 1 round trip di setup vale 100 ms a ogni nuova connessione: verso il tuo dominio, verso la CDN delle immagini, verso il dominio dei font. Sul TTFB e quindi sull’LCP si vede.
HTTP/3 e QUIC: cosa cambia davvero
HTTP/3 (RFC 9114) è la stessa semantica di HTTP — metodi, header, status code — trasportata su QUIC (RFC 9000) invece che su TCP. QUIC gira sopra UDP e integra TLS 1.3 nel protocollo stesso. Le differenze pratiche sono tre.
1. Handshake più corto. Trasporto e crittografia si negoziano insieme: un round trip invece di due. Su una connessione già vista, QUIC permette lo 0-RTT: il client invia la richiesta insieme al primo pacchetto.
2. Niente head-of-line blocking a livello di trasporto. HTTP/2 ha introdotto il multiplexing (più richieste sulla stessa connessione), ma su TCP: se si perde un pacchetto, TCP blocca tutti gli stream finché non viene ritrasmesso, perché garantisce l’ordine dell’intero flusso. Su una rete con perdite, HTTP/2 può andare peggio di HTTP/1.1 con sei connessioni parallele. QUIC gestisce l’ordine per singolo stream: un pacchetto perso rallenta solo la risorsa a cui appartiene.
3. Connection migration. Una connessione TCP è identificata da IP e porta di entrambe le parti: se il telefono passa dal Wi-Fi al 4G, la connessione muore e va rifatta da capo. QUIC usa un connection ID indipendente dall’indirizzo, quindi la connessione sopravvive al cambio di rete. Per un utente che esce di casa con l’app aperta, è la differenza tra un caricamento che continua e uno spinner che riparte.
⚠ Attenzione
Lo 0-RTT ha un costo: i dati inviati nel primo pacchetto possono essere riprodotti da un
attaccante che li intercetta (replay attack). Va abilitato solo per richieste idempotenti come
GET. Il server riceve l’header Early-Data: 1 e può rispondere 425 Too Early per chiedere al
client di ripetere la richiesta dopo l’handshake completo (RFC 8470).
Come il browser scopre che parli HTTP/3
Un browser non prova HTTP/3 alla cieca: la prima connessione avviene su TCP, e il server annuncia il supporto con l’header Alt-Svc. Dalla richiesta successiva il browser passa a QUIC.
alt-svc: h3=":443"; ma=86400
Per saltare anche quel primo giro su TCP esiste il record DNS HTTPS (RFC 9460): il browser lo legge durante la risoluzione del nome e sa già che può usare HTTP/3.
example.com. 3600 IN HTTPS 1 . alpn="h3,h2"
Attivarlo
Nella maggior parte dei casi HTTP/3 lo attivi con un interruttore: Cloudflare, Fastly, CloudFront, Vercel e Netlify lo supportano lato CDN, e il tuo server d’origine può restare su HTTP/1.1 o HTTP/2. Caddy lo abilita di default. Con Nginx (dalla 1.25) servono poche righe:
server {
# QUIC su UDP e TLS su TCP, sulla stessa porta
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
ssl_certificate /etc/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; # QUIC richiede TLS 1.3
# Annuncia HTTP/3 ai browser
add_header Alt-Svc 'h3=":443"; ma=86400' always;
}
Due trappole frequenti: il firewall (o il security group del cloud) deve aprire UDP 443, non solo TCP; e alcune reti aziendali bloccano UDP del tutto. In quel caso il browser ricade su HTTP/2 senza errori, quindi non rompi nulla, ma non vedi neanche il beneficio.
💡 Consiglio
Per verificare: nei DevTools di Chrome, tab Network, clic destro sulle colonne e attiva
“Protocol”. Le risorse servite su QUIC mostrano h3. Da terminale, con un curl compilato con
supporto HTTP/3: curl -sI --http3 https://example.com.
Node.js, al momento in cui scrivo, non ha un server HTTP/3 stabile nel core. Non è un problema: l’architettura normale è terminare HTTP/3 sulla CDN o sul reverse proxy e parlare con Node (o Fastify) in HTTP/1.1 o HTTP/2 sulla rete interna, dove la latenza è trascurabile.
Edge computing: portare il codice vicino all’utente
HTTP/3 riduce il numero di viaggi. L’edge computing riduce la lunghezza di ciascuno. La luce in fibra viaggia a circa 200.000 km/s: tra Milano e un data center in Virginia ci sono quasi 7.000 km, e nessun protocollo scende sotto i ~70 ms di RTT fisico. Le CDN hanno sempre risolto il problema per i file statici; l’edge computing lo estende al codice.
Piattaforme come Cloudflare Workers, Deno Deploy, Netlify Edge Functions, Fastly Compute e CloudFront Functions eseguono funzioni in centinaia di punti di presenza. Il modello tipico usa isolate V8 o WebAssembly invece di container, quindi il cold start è nell’ordine dei millisecondi, non dei secondi.
Cosa funziona bene all’edge
- Redirect, rewrite e header: lingua, A/B test, redirect geografici, header di sicurezza.
- Autenticazione leggera: verificare un JWT o una sessione firmata prima di far passare la richiesta.
- Personalizzazione con cache: la stessa pagina in varianti per paese o valuta, ciascuna in cache.
- Risposte da dati replicati: KV store globali, configurazioni, feature flag.
Un esempio con Cloudflare Workers: una pagina prezzi che cambia valuta in base al paese, servita dalla cache dell’edge per cinque minuti.
// src/worker.ts
interface Env {
API_ORIGIN: string;
}
const CURRENCY: Record<string, string> = { US: "USD", GB: "GBP", CH: "CHF", JP: "JPY" };
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
const country = (request.cf?.country as string | undefined) ?? "IT";
const currency = CURRENCY[country] ?? "EUR";
// Una voce di cache per ogni valuta, non per ogni utente
const cacheKey = new Request(`${url.origin}${url.pathname}?currency=${currency}`);
const cache = caches.default;
const cached = await cache.match(cacheKey);
if (cached) return cached;
const origin = await fetch(`${env.API_ORIGIN}/prices?currency=${currency}`);
const response = new Response(origin.body, origin);
response.headers.set("Cache-Control", "public, max-age=300");
response.headers.set("Vary", "Accept-Encoding");
// Salva in cache senza ritardare la risposta all'utente
ctx.waitUntil(cache.put(cacheKey, response.clone()));
return response;
},
};
Il dettaglio importante è la chiave di cache: la variante dipende dalla valuta, non dal paese né dall’utente. Tre valute significano tre voci in cache per punto di presenza, e la quasi totalità delle richieste non arriva mai all’origine.
La trappola: l’edge lontano dai dati
Qui sta l’errore più comune. Spostare il rendering all’edge è un guadagno solo se i dati sono vicini anche loro. Immagina un utente a Tokyo, una funzione edge a Tokyo e un database Postgres a Francoforte, con un RTT di oltre 200 ms tra i due. Se la pagina fa tre query in sequenza:
| Dove gira il rendering | Utente → funzione | Funzione → database (3 query) | Totale indicativo |
|---|---|---|---|
| Edge a Tokyo | ~5 ms | 3 × ~220 ms | ~665 ms |
| Regione a Francoforte | ~220 ms | 3 × ~1 ms | ~223 ms |
L’edge ha triplicato il tempo. Per questo le piattaforme hanno introdotto correttivi: Cloudflare ha lo Smart Placement, che sposta automaticamente il Worker vicino al backend che chiama più spesso; Vercel oggi consiglia il runtime Node.js in una regione vicina al database per la maggior parte delle funzioni, lasciando l’edge al middleware.
La regola pratica:
- Logica senza dati o con dati in cache/replicati → edge.
- Logica che parla con un database centrale → regione vicina al database, con una CDN davanti per la parte cacheabile.
- Pagine uguali per tutti → nessuna funzione: HTML statico o in cache con
stale-while-revalidate.
Cache-Control: public, max-age=60, stale-while-revalidate=600
Quell’ultima riga è spesso la “ottimizzazione edge” più efficace in assoluto: la CDN serve subito la copia in cache, anche se un po’ vecchia, e la rigenera in background. L’utente non aspetta mai l’origine.
WebAssembly: quando il problema è la CPU
Latenza e distanza riguardano la rete. Ma su un telefono di fascia media anche il calcolo è lento, e un’elaborazione pesante sul main thread fa esplodere l’INP. WebAssembly permette di eseguire codice compilato da Rust, C/C++ o Go a velocità vicina al nativo, nel browser o all’edge.
Casi in cui ha senso:
- Elaborazione di immagini, audio e video lato client (ridimensionare e comprimere una foto prima dell’upload, invece di caricare 8 MB dal telefono).
- Parsing e compressione di file grandi, crittografia, calcoli numerici.
- Riutilizzare una libreria nativa esistente invece di riscriverla (SQLite, codec, motori di layout PDF).
- Inferenza di modelli ML nel browser, di cui ho parlato nell’articolo su WebGPU e AI lato client.
Casi in cui non serve: manipolare il DOM, gestire form, chiamare API. Il JIT di JavaScript è già molto veloce per questo tipo di lavoro, e passare dati avanti e indietro tra JS e Wasm ha un costo. Un modulo Wasm, inoltre, è un download in più.
Il pattern corretto è combinare Wasm con un Web Worker, così il calcolo non blocca nemmeno l’interfaccia. Un esempio: comprimere un’immagine in WebP prima di caricarla, usando i codec di jSquash (gli stessi di Squoosh, compilati in Wasm).
// image-worker.ts
import { encode } from "@jsquash/webp";
self.onmessage = async (event: MessageEvent<ImageData>) => {
// Il codec Wasm viene scaricato e compilato al primo utilizzo
const webp = await encode(event.data, { quality: 75 });
self.postMessage(webp, [webp]); // trasferisce il buffer, nessuna copia
};
// upload.ts
const worker = new Worker(new URL("./image-worker.ts", import.meta.url), { type: "module" });
export async function compress(file: File): Promise<Blob> {
const bitmap = await createImageBitmap(file);
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext("2d")!;
ctx.drawImage(bitmap, 0, 0);
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
return new Promise((resolve) => {
worker.onmessage = (e: MessageEvent<ArrayBuffer>) =>
resolve(new Blob([e.data], { type: "image/webp" }));
worker.postMessage(imageData);
});
}
Una foto da 6 MB scattata dal telefono diventa qualche centinaio di KB: meno upload su rete mobile, meno storage, e l’interfaccia resta reattiva durante la compressione.
ℹ️ Nota
Se il sito usa una Content Security Policy con
script-src, la compilazione di WebAssembly è bloccata finché non aggiungi 'wasm-unsafe-eval'.
È una direttiva specifica per Wasm, molto più ristretta di 'unsafe-eval'. Ricorda anche di
servire i file .wasm con Content-Type: application/wasm, altrimenti
WebAssembly.instantiateStreaming fallisce.
Misurare prima di cambiare infrastruttura
Nessuna di queste tecnologie va attivata “perché è moderna”. Prima servono dati dal campo, non solo da Lighthouse in laboratorio. Con la libreria web-vitals e la Navigation Timing API puoi raccogliere metriche reali insieme al protocollo usato e al tipo di connessione:
// rum.ts
import { onLCP, onINP, onTTFB, type Metric } from "web-vitals";
const nav = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming | undefined;
const context = {
protocol: nav?.nextHopProtocol, // "h3", "h2", "http/1.1"
// Network Information API: solo browser Chromium
connection: (navigator as any).connection?.effectiveType as string | undefined, // "4g", "3g"...
};
function send(metric: Metric) {
const body = JSON.stringify({ name: metric.name, value: metric.value, rating: metric.rating, ...context });
navigator.sendBeacon("/rum", body);
}
onTTFB(send);
onLCP(send);
onINP(send);
Con qualche giorno di dati puoi rispondere a domande concrete: il TTFB p75 degli utenti h3 è migliore di quelli h2? Gli utenti su 3g hanno un LCP fuori soglia? Il problema è il TTFB (rete, server, edge) o quello che succede dopo (JavaScript, immagini)?
Lato server, l’header Server-Timing rende visibile nei DevTools dove si spende il tempo prima del primo byte:
Server-Timing: edge;dur=2, cache;desc="MISS", origin;dur=148, db;dur=97
Se db domina, nessun edge e nessun protocollo ti salveranno: il lavoro è sulle query o sulla cache.
Checklist
Trasporto
- HTTP/3 attivo sulla CDN o sul reverse proxy, UDP 443 aperto
-
Alt-Svcpresente nelle risposte, record DNSHTTPSse il DNS lo supporta - 0-RTT abilitato solo per richieste idempotenti
- Meno domini terzi possibile: ogni nuova origine è un nuovo handshake
Edge e cache
- Asset statici con hash nel nome e
Cache-Control: immutable - HTML cacheabile con
stale-while-revalidatedove il contenuto lo permette - Funzioni edge solo per logica senza dati centrali o con dati replicati
- Rendering dinamico nella regione del database, non “ovunque”
- Chiavi di cache esplicite per le varianti (lingua, valuta), mai per utente
Calcolo
- Lavoro CPU-intensivo spostato in un Web Worker
- WebAssembly solo dove il calcolo è il collo di bottiglia misurato
-
.wasmservito comeapplication/wasm, CSP con'wasm-unsafe-eval'se necessario
Misura
- RUM con LCP, INP, TTFB e protocollo di rete
-
Server-Timingper separare edge, origine e database - Confronto p75 per tipo di connessione, non solo la media
Domande frequenti
❓ HTTP/3 rende il mio sito più veloce?
Sugli utenti con buona connessione fissa la differenza è piccola. Il guadagno si vede su mobile, su reti con perdite di pacchetti e alla prima connessione verso ogni dominio. Visto che si attiva quasi sempre con un’opzione della CDN e ricade su HTTP/2 automaticamente, è uno dei miglioramenti con il miglior rapporto costo/beneficio.
❓ Devo spostare tutta l'applicazione all'edge?
No. L’edge conviene per la logica che non dipende da un database centrale: redirect, header, autenticazione leggera, personalizzazione in cache. Il rendering che fa query va eseguito vicino ai dati, altrimenti ogni query attraversa il pianeta e il risultato è più lento di prima.
❓ WebAssembly è più veloce di JavaScript?
Per calcoli intensivi e prevedibili (codec, compressione, crittografia, numerica) sì, spesso di molto. Per il lavoro tipico di un’interfaccia — DOM, eventi, chiamate di rete — no: JavaScript è già ottimizzato e il passaggio di dati tra i due mondi ha un costo.
❓ Che rapporto c'è con i Core Web Vitals?
HTTP/3 ed edge riducono il TTFB, che è la base dell’LCP. WebAssembly in un Web Worker toglie lavoro dal main thread e migliora l’INP. Nessuno dei tre tocca il CLS, che dipende da come costruisci il layout.
Conclusione
Le performance web si sono spostate dal “quanto pesa la pagina” al “quanti viaggi fa e quanto sono lunghi”. HTTP/3 accorcia e rende più robusti i viaggi, soprattutto su mobile. L’edge computing li rende più corti, a patto che i dati stiano vicini al codice. WebAssembly, in un worker, toglie al telefono il calcolo che lo rallenta. Nessuno dei tre sostituisce le basi — meno JavaScript, immagini giuste, cache fatta bene — ma una volta sistemate quelle sono le leve che separano un sito veloce in ufficio da uno veloce in metropolitana.
Riferimenti:

Co-Fondatore & CTO presso PAPION. Senior full-stack engineer specializzato in React, TypeScript, Node.js e sicurezza applicativa.