Speed Lab: no compression, gzip and Brotli
A real page of HTML, CSS and JavaScript sent uncompressed, with gzip and with Brotli at two levels: bytes, time to first paint and what compression costs.
In this guide
The question
Text compression is one of the quickest fixes on a speed report. The server packs HTML, CSS and JavaScript before it sends them, and the browser unpacks them on arrival. The text compression guide explains how it works and how to switch it on. This test asks how much it is worth on a real page, and whether the choice of method matters.
The page
I wanted real files, not filler. The page is made of:
- HTML: 81,692 bytes of real text, taken from eight guides on this site and set in Bootstrap markup.
- CSS: Bootstrap 5.3.3,
bootstrap.min.css, 232,803 bytes, linked in the head, so the browser must have it before it paints. - JavaScript: Bootstrap's bundle, 80,721 bytes, and D3 7.9.0, 279,706 bytes, both loaded with
defer.
That is 674,922 bytes before compression. There are no images, so the Largest Contentful Paint is a block of text and arrives with the first paint.
The four variants
The local Node server compressed each file with its zlib module when the file was requested, and sent a content-encoding header:
- None: the files as they are.
- gzip, level 6.
- Brotli, quality 5.
- Brotli, quality 11, the highest.
The method and settings are the ones in how the Speed Lab tests are run: Lighthouse 13.5.0, its mobile preset with devtools throttling, plain HTTP/1.1, nine runs per variant taken in turns.
Results
On this page the First Contentful Paint and the Largest Contentful Paint were the same moment, so one column shows both.
| Variant | First paint, median (lowest to highest) | Bytes sent in all | Against none |
|---|---|---|---|
| No compression | 4,296 ms (4,267 to 4,331) | 675,654 | |
| gzip 6 | 1,851 ms (1,809 to 1,896) | 175,658 | 2,445 ms sooner, 74% fewer bytes |
| Brotli 5 | 1,841 ms (1,784 to 1,873) | 164,429 | 2,455 ms sooner, 76% fewer bytes |
| Brotli 11 | 1,649 ms (1,609 to 1,708) | 144,672 | 2,647 ms sooner, 79% fewer bytes |
Cumulative Layout Shift was 0 in every run, and Total Blocking Time stayed under 60 ms.
Three results stand out.
- Compression more than halved the time to first paint. From 4,296 ms to 1,851 ms with plain gzip, for one setting at the server.
- gzip and Brotli 5 are the same here. The ranges overlap, so I treat them as equal, even though Brotli sent about 11,000 fewer bytes.
- Brotli 11 was clearly faster. Its range (1,609 to 1,708) does not overlap with gzip's (1,809 to 1,896).
Which file matters
The bytes sent for each file, as measured in the runs:
| File | Not compressed | gzip 6 | Brotli 5 | Brotli 11 |
|---|---|---|---|---|
| HTML | 81,883 | 26,829 | 25,719 | 22,536 |
bootstrap.min.css | 232,979 | 31,028 | 28,447 | 22,906 |
| Bootstrap bundle | 80,903 | 24,064 | 23,152 | 21,374 |
d3.min.js | 279,889 | 93,737 | 87,111 | 77,856 |
The sizes include about 200 bytes of headers for each file. The CSS shrank the most, to 13% of its size with gzip and 10% with Brotli 11, because a style sheet is full of repeated words.
The Lighthouse timeline shows why the CSS matters most. The browser cannot paint until the style sheet has arrived, and the first paint came 213 to 246 ms after it. The style sheet finished at 4,076 ms with no compression, 1,638 with gzip, 1,595 with Brotli 5 and 1,424 with Brotli 11. The first paint tracked those numbers. The two scripts are loaded with defer, so they do not hold the paint. They do download at the same time as the style sheet and share the limited connection with it, so smaller scripts probably help the paint too. That is my reading of the timeline. See critical CSS and render-blocking.
What compression costs the server
The table above hides a price. I timed how long the compression itself takes for each file, on the same machine, as the median of five runs:
| File | gzip 6 | gzip 9 | Brotli 5 | Brotli 11 |
|---|---|---|---|---|
| HTML | 3 ms | 3 ms | 2 ms | 115 ms |
bootstrap.min.css | 3 ms | 5 ms | 3 ms | 344 ms |
| Bootstrap bundle | 2 ms | 2 ms | 2 ms | 119 ms |
d3.min.js | 7 ms | 10 ms | 9 ms | 545 ms |
Brotli at its highest level took more than 50 times longer than the middle level, for 11% to 26% fewer bytes than gzip. If a server did that on every request for a page made on demand, the visitor would wait that long for the first byte. A style sheet or a script that never changes is different: compress it once, at the top level, and serve the saved copy. Many servers and CDNs can keep a compressed copy of static files, so check how yours does it.
In my page timings this cost does not show. The server compressed the files on the fly, even at level 11, and Brotli 11 still painted first. The likely reason is the throttling: in devtools mode every request waits at least 562.5 ms for its response, which is longer than any compression time in the table, so the server's work seems to have happened inside that wait. On a real connection that time would be added to the wait for the file. That is the reason to time the compression separately, as I did.
What this test does not show
- A server under load. One request at a time is easy. A busy server that compresses hundreds of pages a second feels the processor cost.
- Other kinds of page. A page with less CSS and script gains less. A page with more gains more. Images and video do not shrink this way.
- What the browser asks for. My server sent the chosen encoding without reading the
Accept-Encodingheader. A real server picks a method the browser says it can unpack, as the compression guide explains. - The whole network. The lab is one simulated phone on a local server. See the method page.
- Caching. Each run started with an empty cache. A returning visitor may not need to download these files at all. See HTTP caching headers.
What to do
- Check that your files are compressed. Look at the
content-encodingheader of the page, one CSS file and one script, with the Network panel orcurl. The text compression guide gives the commands. - Turn it on at the server or the CDN, for HTML, CSS, JavaScript, JSON and SVG. Not for images and video.
- Prefer Brotli, with gzip for browsers that cannot read it. At a middle level it costs about what gzip costs.
- Use the top Brotli level only for files you compress once, such as the CSS and JavaScript built for a release.
- Check the result with the audit's compression finding, and compare the bytes before and after.
Run it yourself
To see how much your own files shrink and what each method costs, save this as compress-sizes.mjs and run node compress-sizes.mjs index.html style.css app.js with your file names. It needs only Node.
// compress-sizes.mjs: node compress-sizes.mjs index.html style.css app.js
// Prints the size of each file as it is, with gzip and with Brotli, and how long each way took (median of five).
import { readFileSync } from 'node:fs';
import { gzipSync, brotliCompressSync, constants } from 'node:zlib';
const brotli = (quality) => (buf) => brotliCompressSync(buf, { params: { [constants.BROTLI_PARAM_QUALITY]: quality } });
const ways = { 'gzip 6': (buf) => gzipSync(buf, { level: 6 }), 'brotli 5': brotli(5), 'brotli 11': brotli(11) };
const median = (xs) => [...xs].sort((a, b) => a - b)[xs.length >> 1];
const rows = process.argv.slice(2).map((file) => {
const buf = readFileSync(file);
const row = { file, bytes: buf.length };
for (const [name, compress] of Object.entries(ways)) {
const ms = [];
let size = 0;
for (let i = 0; i < 5; i++) {
const start = process.hrtime.bigint();
size = compress(buf).length;
ms.push(Number(process.hrtime.bigint() - start) / 1e6);
}
row[`${name} bytes`] = size;
row[`${name} ms`] = Math.round(median(ms));
}
return row;
});
console.table(rows);On the files above it printed 26,614, 25,506 and 22,323 bytes for the HTML, in 3, 2 and 115 ms. The sizes are those of the compressed data alone, without the headers.
Mistakes to avoid
- Compressing images and video with a text method. They are already packed, and the processor time is wasted.
- Using the top Brotli level on every request for pages made on demand.
- Compressing in two places, such as the application and the CDN. Pick one.
- Forgetting `Vary: Accept-Encoding`, so that a cache hands a packed file to a client that cannot read it.
- Assuming it is on. Check the header.
Check your own page
Run the free audit and look for the text compression finding on the page, the style sheets and the scripts. If any are sent without compression, switch it on, run the audit again and compare the size and the first paint.
Sources
- Compression in HTTP on MDN: how browser and server agree on a method, and which files to compress.
- Content-Encoding on MDN: the header that names the method a response was packed with.
- The earlier text compression guide and its sources, for gzip, Brotli and Zstandard.
- Bootstrap 5.3.3 and D3 7.9.0 as published on npm, used as real test files.
Start here
Free website audit
Live Google data for any public page: mobile and desktop.
Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it
Rather have this done for you? Speed optimization · Book a call