Measuring Protocol Impact with WebPageTest
Measuring Protocol Impact with WebPageTest
The Symptom
The team enabled HTTP/2 and Brotli on the production server but is not sure whether the changes are effective. Chrome DevTools shows h2 in the protocol column, but that only confirms negotiation, not benefit. The LCP improved by 300ms, but was that from HTTP/2 multiplexing or Brotli compression? Understanding the attribution is necessary to prioritize future infrastructure work.
The Cause
Protocol-level improvements overlap in their effects. HTTP/2 multiplexing reduces round-trip waste. Brotli reduces transfer size. Both contribute to faster resource delivery. Without isolating each variable, you cannot determine which investment produced the return.
WebPageTest provides the granularity to separate these effects. Its connection view shows how streams are multiplexed. Its content encoding column shows whether Brotli or gzip was negotiated. Running tests with controlled variations isolates each factor.
The Baseline
WebPageTest test matrix for the e-commerce listing page from Virginia, Moto G Power, 4G:
| Configuration | LCP | TTFB | Total Transfer | Connections |
|---|---|---|---|---|
| HTTP/1.1 + gzip | 4.1s | 680ms | 244 kB | 6 parallel |
| HTTP/2 + gzip | 3.7s | 680ms | 244 kB | 1 multiplexed |
| HTTP/2 + Brotli | 3.4s | 680ms | 200 kB | 1 multiplexed |
| HTTP/3 + Brotli | 3.2s | 420ms | 200 kB | 1 QUIC |
Isolating each factor:
- HTTP/1.1 → HTTP/2 (gzip held constant): -400ms LCP from multiplexing
- gzip → Brotli (HTTP/2 held constant): -300ms LCP from smaller transfer
- HTTP/2 → HTTP/3 (Brotli held constant): -200ms LCP from QUIC handshake and reduced HOL blocking
The Fix: Verification Methodology
Run each test three times and take the median to reduce variance. WebPageTest’s “rerun” feature makes this straightforward.
For protocol verification, check the connection view:
Connection 1 (h2, *.example.com):
Stream 1: index.html (12 kB, 180ms)
Stream 3: main.css (34 kB, 120ms)
Stream 5: vendor.js (92 kB, 280ms)
Stream 7: index.js (64 kB, 210ms)
Stream 9: product-listing.js (38 kB, 150ms)
Stream 11: hero-image.avif (42 kB, 180ms)
... (18 more streams)
All 28 resources share one connection with interleaved streams. In HTTP/1.1, the same resources would show 6 connections with sequential requests.
For Brotli verification, check the response headers:
Content-Encoding: br
Content-Type: application/javascript
Content-Length: 64218
The Content-Encoding: br header confirms Brotli is being served. If you see Content-Encoding: gzip, the Brotli configuration is not working. Common causes:
- The
Accept-Encodingrequest header does not includebr(unlikely with modern browsers) - The server module is not loaded (
ngx_brotliin Nginx) - The
.brpre-compressed files are not in the expected location - The server’s MIME type configuration does not include the file type in Brotli-eligible types
For compression ratio validation:
// scripts/validate-compression.ts
import * as fs from "fs";
import * as path from "path";
interface CompressionReport {
file: string;
original: number;
gzip: number;
brotli: number;
gzipRatio: number;
brotliRatio: number;
brotliVsGzip: number;
}
function validateCompression(distDir: string): CompressionReport[] {
const reports: CompressionReport[] = [];
const files = fs.readdirSync(distDir, { recursive: true }) as string[];
for (const file of files) {
const fullPath = path.join(distDir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) continue;
const brPath = `${fullPath}.br`;
const gzPath = `${fullPath}.gz`;
if (!fs.existsSync(brPath) || !fs.existsSync(gzPath)) continue;
const originalSize = stat.size;
const gzipSize = fs.statSync(gzPath).size;
const brotliSize = fs.statSync(brPath).size;
reports.push({
file,
original: originalSize,
gzip: gzipSize,
brotli: brotliSize,
gzipRatio: (gzipSize / originalSize) * 100,
brotliRatio: (brotliSize / originalSize) * 100,
brotliVsGzip: ((gzipSize - brotliSize) / gzipSize) * 100,
});
}
return reports.sort((a, b) => b.original - a.original);
}
const reports = validateCompression("dist");
console.log("Compression Report:");
console.log("| File | Original | Gzip | Brotli | Brotli vs Gzip |");
console.log("|------|----------|------|--------|----------------|");
let totalOriginal = 0;
let totalGzip = 0;
let totalBrotli = 0;
for (const r of reports.slice(0, 20)) {
console.log(
`| ${r.file} | ${(r.original / 1024).toFixed(1)} kB | ` +
`${(r.gzip / 1024).toFixed(1)} kB | ` +
`${(r.brotli / 1024).toFixed(1)} kB | ` +
`${r.brotliVsGzip.toFixed(1)}% smaller |`,
);
totalOriginal += r.original;
totalGzip += r.gzip;
totalBrotli += r.brotli;
}
console.log(
`\nTotals: Original ${(totalOriginal / 1024).toFixed(0)} kB, ` +
`Gzip ${(totalGzip / 1024).toFixed(0)} kB, ` +
`Brotli ${(totalBrotli / 1024).toFixed(0)} kB ` +
`(${(((totalGzip - totalBrotli) / totalGzip) * 100).toFixed(1)}% smaller than gzip)`,
);
The Proof
The validation script run against the e-commerce platform build output:
Compression Report:
| File | Original | Gzip | Brotli | Brotli vs Gzip |
|------|----------|------|--------|----------------|
| vendor.js | 448.2 kB | 112.3 kB | 92.1 kB | 18.0% smaller |
| main.js | 312.4 kB | 78.2 kB | 64.0 kB | 18.2% smaller |
| main.css | 186.0 kB | 42.1 kB | 34.2 kB | 18.8% smaller |
| dashboard.js | 180.4 kB | 48.2 kB | 39.8 kB | 17.4% smaller |
Totals: Original 1126 kB, Gzip 281 kB, Brotli 230 kB (18.1% smaller than gzip)
Brotli consistently saves 17-19% over gzip for text-based assets. For the total transfer, this is 51KB saved, which at 4G throughput (1.6 Mbps) translates to 255ms of download time.
WebPageTest filmstrip comparison (Virginia, 4G, first view):
| Time | HTTP/1.1 + gzip | HTTP/3 + Brotli |
|---|---|---|
| 1.0s | White screen | White screen |
| 2.0s | Header only | Header + product grid skeleton |
| 3.0s | Partial product grid | Full product grid with images |
| 4.0s | Full product grid | Page fully interactive |
| 4.1s | LCP | (LCP was at 3.2s) |
The filmstrip makes the 900ms combined improvement visible. At the 3-second mark, the HTTP/3 + Brotli configuration shows a fully rendered page. The HTTP/1.1 + gzip configuration shows a partial page still loading.
The Trade-off
HTTP/3 support varies by CDN and hosting provider. Not all CDNs support QUIC. If your hosting does not support HTTP/3, you still get the full benefit of HTTP/2 + Brotli, which provides 700ms of the 900ms total improvement.
Brotli compression at quality 11 is CPU-intensive at build time. For CI runners with limited CPU, the 15-second compression step adds meaningful time to the build. Reducing to quality 6 produces files 3-5% larger than quality 11 but compresses 5x faster. The tradeoff: 2-3KB additional transfer per resource vs 12 seconds of CI time saved. For the e-commerce platform with 15 daily builds, quality 11 was retained because the total bandwidth saving across all users per day far exceeds the CI compute cost.
Protocol improvements have diminishing returns. After enabling HTTP/2, HTTP/3, and Brotli, the next protocol-level optimization (QUIC 0-RTT resumption) provides single-digit millisecond improvements that are not measurable in field data. The optimization effort shifts to application-level changes: bundle size (Chapter 3), image optimization (Chapter 4), and caching strategy (Chapter 8).