Diego Betto
Photo by Steven Lelham on Unsplash

Diego Betto · October 18, 2026 · 14 min di lettura

Performance, HTTP/3 and edge computing: why latency has become a competitive advantage

HTTP/3 over QUIC, edge computing and WebAssembly explained with practical examples: what they really improve on mobile, how to enable them and when they don't help.

Condividi:XLinkedInFacebookWhatsApp

For years “optimizing performance” mostly meant cutting weight: less JavaScript, compressed images, inlined critical CSS. All of that still holds, and I covered it with real numbers in the article on Core Web Vitals. But on mobile, where most traffic now comes from, the bottleneck often isn’t bandwidth: it’s latency. Every round trip between phone and server costs tens or hundreds of milliseconds, and a page makes many of them before showing anything.

Three technologies attack exactly that problem, each from a different side:

Technology What it reduces Where it acts
HTTP/3 + QUIC Connection round trips, stalls on unstable networks Transport, between client and server
Edge computing Physical distance between user and code Infrastructure, where the server runs
WebAssembly Compute time for CPU-heavy work Execution, in the browser or at the edge

In this article we look at what each one really does, how to turn it on, how to measure whether you need it and — just as important — when it changes nothing.

Why milliseconds are a business topic

The link between speed and conversions isn’t a developer hunch. The Milliseconds Make Millions study (Deloitte for Google, 2020) found that a 0.1 second improvement in mobile site speed was associated with an 8.4% increase in conversions in retail and 10.1% in travel. Treat those figures as an order of magnitude rather than a promise, but the direction has been confirmed by almost every case study published since.

The less obvious part is where that time is lost. On a 4G connection the round-trip time (RTT) to a server is typically 50–100 ms; on a congested network or on the move it easily exceeds 200 ms. What matters is how many trips happen before the first useful byte:

Phase HTTP/1.1 or HTTP/2 over TCP + TLS 1.3 HTTP/3 over QUIC
Transport handshake 1 RTT (TCP) Combined
Cryptographic handshake 1 RTT (TLS 1.3) 1 RTT in total
Resuming a connection 1 RTT (TCP) + TLS resumption 0 RTT possible
Request and response 1 RTT 1 RTT

With a 100 ms RTT, going from 2 to 1 setup round trips is worth 100 ms on every new connection: to your domain, to the image CDN, to the font domain. It shows up in TTFB, and therefore in LCP.

HTTP/3 and QUIC: what actually changes

HTTP/3 (RFC 9114) keeps the same HTTP semantics — methods, headers, status codes — carried over QUIC (RFC 9000) instead of TCP. QUIC runs on top of UDP and builds TLS 1.3 into the protocol itself. There are three practical differences.

1. Shorter handshake. Transport and encryption are negotiated together: one round trip instead of two. On a connection it has seen before, QUIC allows 0-RTT: the client sends the request along with its very first packet.

2. No transport-level head-of-line blocking. HTTP/2 introduced multiplexing (many requests over one connection), but on top of TCP: if a packet is lost, TCP stalls every stream until it’s retransmitted, because it guarantees ordering for the whole byte stream. On a lossy network HTTP/2 can do worse than HTTP/1.1 with six parallel connections. QUIC handles ordering per stream: a lost packet only delays the resource it belongs to.

3. Connection migration. A TCP connection is identified by the IP and port of both ends: when a phone switches from Wi-Fi to 4G, the connection dies and has to be rebuilt from scratch. QUIC uses a connection ID that doesn’t depend on the address, so the connection survives the network change. For a user leaving home with the app open, it’s the difference between a load that keeps going and a spinner that starts over.

⚠ Attenzione

0-RTT has a cost: data sent in the first packet can be replayed by an attacker who captures it (replay attack). Enable it only for idempotent requests such as GET. The server receives the Early-Data: 1 header and can answer 425 Too Early to ask the client to retry after the full handshake (RFC 8470).

How the browser discovers you speak HTTP/3

A browser doesn’t try HTTP/3 blindly: the first connection goes over TCP, and the server advertises support with the Alt-Svc header. From the next request on, the browser switches to QUIC.

alt-svc: h3=":443"; ma=86400

To skip even that first TCP trip there is the DNS HTTPS record (RFC 9460): the browser reads it while resolving the name and already knows it can use HTTP/3.

example.com.  3600  IN  HTTPS  1  .  alpn="h3,h2"

Turning it on

Most of the time HTTP/3 is a switch you flip: Cloudflare, Fastly, CloudFront, Vercel and Netlify support it at the CDN, and your origin server can stay on HTTP/1.1 or HTTP/2. Caddy enables it by default. With Nginx (1.25 and later) it takes a few lines:

server {
    # QUIC over UDP and TLS over TCP, on the same port
    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 requires TLS 1.3

    # Advertise HTTP/3 to browsers
    add_header Alt-Svc 'h3=":443"; ma=86400' always;
}

Two common traps: the firewall (or cloud security group) must open UDP 443, not just TCP; and some corporate networks block UDP entirely. In that case the browser silently falls back to HTTP/2, so nothing breaks, but you don’t get the benefit either.

💡 Consiglio

To check: in Chrome DevTools, Network tab, right-click the column headers and enable “Protocol”. Resources served over QUIC show h3. From the terminal, with a curl built with HTTP/3 support: curl -sI --http3 https://example.com.

At the time of writing, Node.js has no stable HTTP/3 server in core. That’s fine: the normal architecture is to terminate HTTP/3 at the CDN or reverse proxy and talk to Node (or Fastify) over HTTP/1.1 or HTTP/2 on the internal network, where latency is negligible.

Edge computing: moving code close to the user

HTTP/3 reduces the number of trips. Edge computing reduces the length of each one. Light in fiber travels at roughly 200,000 km/s: between Milan and a data center in Virginia there are almost 7,000 km, and no protocol gets below a ~70 ms physical RTT. CDNs have always solved this for static files; edge computing extends it to code.

Platforms such as Cloudflare Workers, Deno Deploy, Netlify Edge Functions, Fastly Compute and CloudFront Functions run functions in hundreds of points of presence. The typical model uses V8 isolates or WebAssembly instead of containers, so cold starts are measured in milliseconds, not seconds.

What works well at the edge

  • Redirects, rewrites and headers: language, A/B tests, geographic redirects, security headers.
  • Lightweight authentication: verifying a JWT or a signed session before letting the request through.
  • Cached personalization: the same page in variants per country or currency, each one cached.
  • Responses from replicated data: global KV stores, configuration, feature flags.

An example with Cloudflare Workers: a pricing page whose currency depends on the country, served from the edge cache for five minutes.

// 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";

    // One cache entry per currency, not per user
    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");

    // Store in cache without delaying the response to the user
    ctx.waitUntil(cache.put(cacheKey, response.clone()));
    return response;
  },
};

The key detail is the cache key: the variant depends on the currency, not on the country or the user. Three currencies mean three cache entries per point of presence, and almost no requests ever reach the origin.

The trap: an edge far away from the data

This is the most common mistake. Moving rendering to the edge is a win only if the data is close too. Picture a user in Tokyo, an edge function in Tokyo and a Postgres database in Frankfurt, with an RTT of over 200 ms between them. If the page runs three queries in sequence:

Where rendering runs User → function Function → database (3 queries) Rough total
Edge in Tokyo ~5 ms 3 × ~220 ms ~665 ms
Region in Frankfurt ~220 ms 3 × ~1 ms ~223 ms

The edge tripled the time. That’s why platforms have added correctives: Cloudflare has Smart Placement, which automatically moves the Worker close to the backend it calls most; Vercel now recommends the Node.js runtime in a region near the database for most functions, leaving the edge to middleware.

The rule of thumb:

  • Logic with no data, or with cached/replicated data → edge.
  • Logic that talks to a central database → a region near the database, with a CDN in front for the cacheable part.
  • Pages that are the same for everyone → no function at all: static HTML, or cached with stale-while-revalidate.
Cache-Control: public, max-age=60, stale-while-revalidate=600

That last line is often the single most effective “edge optimization”: the CDN immediately serves the cached copy, even if slightly stale, and regenerates it in the background. The user never waits for the origin.

WebAssembly: when the problem is the CPU

Latency and distance are about the network. But on a mid-range phone compute is slow too, and heavy processing on the main thread blows up INP. WebAssembly lets you run code compiled from Rust, C/C++ or Go at near-native speed, in the browser or at the edge.

Cases where it makes sense:

  • Client-side image, audio and video processing (resizing and compressing a photo before upload instead of sending 8 MB from the phone).
  • Parsing and compressing large files, cryptography, numeric computation.
  • Reusing an existing native library instead of rewriting it (SQLite, codecs, PDF layout engines).
  • Running ML models in the browser, which I covered in the article on WebGPU and client-side AI.

Cases where you don’t need it: manipulating the DOM, handling forms, calling APIs. The JavaScript JIT is already very fast for that kind of work, and passing data back and forth between JS and Wasm has a cost. A Wasm module is also one more download.

The right pattern is to pair Wasm with a Web Worker, so the computation doesn’t block the UI either. An example: compressing an image to WebP before uploading it, using jSquash’s codecs (the same ones as Squoosh, compiled to Wasm).

// image-worker.ts
import { encode } from "@jsquash/webp";

self.onmessage = async (event: MessageEvent<ImageData>) => {
  // The Wasm codec is downloaded and compiled on first use
  const webp = await encode(event.data, { quality: 75 });
  self.postMessage(webp, [webp]); // transfer the buffer, no copy
};
// 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);
  });
}

A 6 MB photo taken with a phone turns into a few hundred KB: less upload over a mobile network, less storage, and the UI stays responsive during compression.

ℹ️ Nota

If the site uses a Content Security Policy with script-src, WebAssembly compilation is blocked until you add 'wasm-unsafe-eval'. It’s a Wasm-specific directive, much narrower than 'unsafe-eval'. Also remember to serve .wasm files with Content-Type: application/wasm, otherwise WebAssembly.instantiateStreaming fails.

Measure before changing infrastructure

None of these technologies should be turned on “because it’s modern”. First you need field data, not just Lighthouse in the lab. With the web-vitals library and the Navigation Timing API you can collect real metrics together with the protocol used and the connection type:

// 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: Chromium browsers only
  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);

With a few days of data you can answer concrete questions: is p75 TTFB for h3 users better than for h2 users? Do users on 3g have an LCP outside the threshold? Is the problem TTFB (network, server, edge) or what happens afterwards (JavaScript, images)?

On the server side, the Server-Timing header makes it visible in DevTools where time is spent before the first byte:

Server-Timing: edge;dur=2, cache;desc="MISS", origin;dur=148, db;dur=97

If db dominates, no edge and no protocol will save you: the work is on the queries or on caching.

Checklist

Transport

  • HTTP/3 enabled on the CDN or reverse proxy, UDP 443 open
  • Alt-Svc present in responses, DNS HTTPS record if your DNS supports it
  • 0-RTT enabled only for idempotent requests
  • As few third-party domains as possible: every new origin is a new handshake

Edge and caching

  • Static assets with hashed names and Cache-Control: immutable
  • Cacheable HTML with stale-while-revalidate where the content allows it
  • Edge functions only for logic without central data, or with replicated data
  • Dynamic rendering in the database’s region, not “everywhere”
  • Explicit cache keys for variants (language, currency), never per user

Compute

  • CPU-heavy work moved to a Web Worker
  • WebAssembly only where compute is the measured bottleneck
  • .wasm served as application/wasm, CSP with 'wasm-unsafe-eval' if needed

Measurement

  • RUM with LCP, INP, TTFB and network protocol
  • Server-Timing to separate edge, origin and database
  • p75 comparisons by connection type, not just the average

Frequently asked questions

❓ Does HTTP/3 make my site faster?

For users on a good fixed connection the difference is small. The gain shows on mobile, on lossy networks and on the first connection to each domain. Since it’s almost always a CDN option and falls back to HTTP/2 automatically, it’s one of the best cost/benefit improvements available.

❓ Should I move my whole application to the edge?

No. The edge pays off for logic that doesn’t depend on a central database: redirects, headers, lightweight authentication, cached personalization. Rendering that runs queries belongs near the data, otherwise every query crosses the planet and the result is slower than before.

❓ Is WebAssembly faster than JavaScript?

For intensive, predictable computation (codecs, compression, cryptography, numerics) yes, often by a lot. For typical UI work — DOM, events, network calls — no: JavaScript is already optimized and moving data between the two worlds has a cost.

❓ How does this relate to Core Web Vitals?

HTTP/3 and the edge reduce TTFB, which is the foundation of LCP. WebAssembly in a Web Worker takes work off the main thread and improves INP. None of the three affects CLS, which depends on how you build the layout.

Conclusion

Web performance has shifted from “how heavy is the page” to “how many trips does it make and how long are they”. HTTP/3 makes trips fewer and more robust, especially on mobile. Edge computing makes them shorter, as long as the data sits close to the code. WebAssembly, inside a worker, takes away the computation that slows the phone down. None of them replaces the basics — less JavaScript, the right images, good caching — but once those are in place, they are the levers that separate a site that’s fast at the office from one that’s fast on the subway.

References:

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Written by

Diego Betto

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.