HTTP/2 vs HTTP/3 Latency Modeler

function calcH2(rttMs, lossFrac, bwBps, streams, pageBytes) { const mss = 1460; // bytes const rttS = rttMs / 1000; // Mathis throughput per stream const mathisBps = lossFrac > 0 ? Math.min((mss / rttS) * (1 / Math.sqrt(lossFrac)), bwBps / streams) : bwBps / streams; const totalBw = mathisBps * Math.min(streams, 6); // browser typically 6 TCP connections for HTTP/2 // Connection setup: 1-RTT TCP + 1-RTT TLS1.3 const setupRtts = 2; const transferS = pageBytes / totalBw; // HoL blocking penalty: estimated as avg retransmit timeout * loss probability per stream const rtoMs = Math.max(rttMs * 1.5, 200); // RTO ~= 1.5 RTT, min 200ms const holPenaltyS = (lossFrac * rtoMs / 1000) * streams; const totalS = setupRtts * rttS + transferS + holPenaltyS; return { totalMs: totalS * 1000, throughputMbps: (totalBw / 1e6), setupMs: setupRtts * rttMs, transferMs: transferS * 1000, holMs: holPenaltyS * 1000 }; } function calcH3(rttMs, lossFrac, bwBps, streams, pageBytes) { const mss = 1200; // QUIC uses smaller initial PMTU const rttS = rttMs / 1000; // QUIC per-stream Mathis, but no HoL blocking -- each stream recovers independently const mathisPerStream = lossFrac > 0 ? Math.min((mss / rttS) * (1 / Math.sqrt(lossFrac)), bwBps / streams) : bwBps / streams; const totalBw = mathisPerStream * streams; // all streams can progress independently // 0-RTT resumption for repeat visitors, 1-RTT for new const setupRtts = 1; const transferS = pageBytes / totalBw; // No HoL -- only the one stream with the lost packet stalls briefly const rtoMs = Math.max(rttMs * 1.5, 200); const holPenaltyS = (lossFrac * rtoMs / 1000) * 1; // only 1 stream stalls const totalS = setupRtts * rttS + transferS + holPenaltyS; return { totalMs: totalS * 1000, throughputMbps: (totalBw / 1e6), setupMs: setupRtts * rttMs, transferMs: transferS * 1000, holMs: holPenaltyS * 1000 }; } function model() { const rttMs = parseFloat(document.getElementById('rttMs').value); const lossPct = parseFloat(document.getElementById('lossRate').value); const bwMbps = parseFloat(document.getElementById('bwMbps').value); const streams = parseInt(document.getElementById('streams').value); const pageKb = parseFloat(document.getElementById('pageKb').value); const div = document.getElementById('modelResult'); div.style.display = 'block'; if (isNaN(rttMs) || isNaN(lossPct) || isNaN(bwMbps) || rttMs <= 0 || bwMbps <= 0) { div.innerHTML = 'Invalid input.'; return; } const lossFrac = lossPct / 100; const bwBps = bwMbps * 1e6; const pageBytes = pageKb * 1024; const h2 = calcH2(rttMs, lossFrac, bwBps, streams, pageBytes); const h3 = calcH3(rttMs, lossFrac, bwBps, streams, pageBytes); const winner = h2.totalMs < h3.totalMs * 0.95 ? 'h2' : h3.totalMs < h2.totalMs * 0.95 ? 'h3' : 'tie'; const fmt = n => n < 1000 ? `${Math.round(n)} ms` : `${(n/1000).toFixed(2)} s`; div.innerHTML = `
Latency Model Results

HTTP/2 (TCP)

Total page load${fmt(h2.totalMs)}
Effective throughput${h2.throughputMbps.toFixed(2)} Mbps
Handshake overhead${fmt(h2.setupMs)}
Transfer time${fmt(h2.transferMs)}
HoL blocking penalty${fmt(h2.holMs)}

HTTP/3 (QUIC)

Total page load${fmt(h3.totalMs)}
Effective throughput${h3.throughputMbps.toFixed(2)} Mbps
Handshake overhead${fmt(h3.setupMs)}
Transfer time${fmt(h3.transferMs)}
HoL blocking penalty${fmt(h3.holMs)}
${winner === 'h2' ? 'HTTP/2 is faster by ' + fmt(h3.totalMs - h2.totalMs) + ' in this scenario' : winner === 'h3' ? 'HTTP/3 is faster by ' + fmt(h2.totalMs - h3.totalMs) + ' in this scenario' : 'Within 5% -- effectively a tie in this scenario'}
Model uses Mathis equation for TCP throughput. HoL blocking penalty: HTTP/2 estimates RTO × loss × streams; HTTP/3 only 1 stream stalls. Actual results vary by congestion algorithm, QUIC implementation, and network conditions.
`; } function modelCurve() { const rttMs = parseFloat(document.getElementById('rttMs').value) || 80; const bwMbps = parseFloat(document.getElementById('bwMbps').value) || 10; const streams = parseInt(document.getElementById('streams').value) || 6; const pageKb = parseFloat(document.getElementById('pageKb').value) || 500; const bwBps = bwMbps * 1e6; const pageBytes = pageKb * 1024; const lossPoints = [0, 0.1, 0.5, 1, 2, 3, 5, 10]; const div = document.getElementById('lossCurve'); div.style.display = 'block'; const maxMs = Math.max(...lossPoints.map(l => { const h2 = calcH2(rttMs, l/100, bwBps, streams, pageBytes); const h3 = calcH3(rttMs, l/100, bwBps, streams, pageBytes); return Math.max(h2.totalMs, h3.totalMs); })); const rows = lossPoints.map(l => { const h2 = calcH2(rttMs, l/100, bwBps, streams, pageBytes); const h3 = calcH3(rttMs, l/100, bwBps, streams, pageBytes); const h2w = Math.round((h2.totalMs / maxMs) * 100); const h3w = Math.round((h3.totalMs / maxMs) * 100); const fmt = n => n < 1000 ? `${Math.round(n)}ms` : `${(n/1000).toFixed(1)}s`; const winnerLbl = h2.totalMs < h3.totalMs * 0.95 ? '< H2' : h3.totalMs < h2.totalMs * 0.95 ? 'H3 >' : '~tie'; return `
${l}%
${fmt(h2.totalMs)}
${fmt(h3.totalMs)} ${winnerLbl}
`; }).join(''); div.innerHTML = `
Loss % HTTP/2 HTTP/3
${rows}`; }