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.

IntermediateHow-to6 min readUpdated October 30, 2026

In this guide
  1. The question
  2. The page
    1. The four variants
  3. Results
    1. Which file matters
  4. What compression costs the server
  5. What this test does not show
  6. What to do
  7. Run it yourself
  8. Mistakes to avoid
  9. Check your own page
  10. Sources

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:

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:

  1. None: the files as they are.
  2. gzip, level 6.
  3. Brotli, quality 5.
  4. 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.

VariantFirst paint, median (lowest to highest)Bytes sent in allAgainst none
No compression4,296 ms (4,267 to 4,331)675,654
gzip 61,851 ms (1,809 to 1,896)175,6582,445 ms sooner, 74% fewer bytes
Brotli 51,841 ms (1,784 to 1,873)164,4292,455 ms sooner, 76% fewer bytes
Brotli 111,649 ms (1,609 to 1,708)144,6722,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.

  1. 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.
  2. 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.
  3. 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:

FileNot compressedgzip 6Brotli 5Brotli 11
HTML81,88326,82925,71922,536
bootstrap.min.css232,97931,02828,44722,906
Bootstrap bundle80,90324,06423,15221,374
d3.min.js279,88993,73787,11177,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:

Filegzip 6gzip 9Brotli 5Brotli 11
HTML3 ms3 ms2 ms115 ms
bootstrap.min.css3 ms5 ms3 ms344 ms
Bootstrap bundle2 ms2 ms2 ms119 ms
d3.min.js7 ms10 ms9 ms545 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

What to do

  1. Check that your files are compressed. Look at the content-encoding header of the page, one CSS file and one script, with the Network panel or curl. The text compression guide gives the commands.
  2. Turn it on at the server or the CDN, for HTML, CSS, JavaScript, JSON and SVG. Not for images and video.
  3. Prefer Brotli, with gzip for browsers that cannot read it. At a middle level it costs about what gzip costs.
  4. Use the top Brotli level only for files you compress once, such as the CSS and JavaScript built for a release.
  5. 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

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

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

Related guides

All guides →