⚡ Web performance

Brotli and gzip compression: levels, precompression and checks

How HTTP response compression works, when to choose Brotli or gzip, which levels to use for static and dynamic files, and how to verify that it works.

HTML, CSS, JavaScript, JSON and SVG are text, and text compresses very well. Enabling response compression is one of the changes with the best effort-to-benefit ratio: often a single line of configuration reduces the transferred weight of these files to a fraction of the original. Yet you still find sites that send uncompressed JavaScript, or that compress JPEG images, wasting CPU for no gain.

How negotiation works

The browser declares which algorithms it can decode with the Accept-Encoding header:

Accept-Encoding: gzip, deflate, br, zstd

The server picks one of them, compresses the response body and signals it with Content-Encoding:

Content-Encoding: br
Vary: Accept-Encoding

The Vary header matters: it tells intermediate caches that the same resource exists in different versions depending on what the client accepts, so a cache will not serve the Brotli version to a client that only knows gzip.

gzip and Brotli compared

Feature gzip Brotli
Levels 1 to 9 0 to 11
Support universal all modern browsers, HTTPS only
Compression ratio good usually better on web text
Speed at high levels fast very slow at levels 10-11
Decompression fast fast

Brotli includes a built-in dictionary of frequent HTML, CSS and JavaScript fragments, which gives it an edge especially on small files. The gain over gzip varies from file to file, but on web text it is usually noticeable. More recently some browsers have added support for Zstandard (zstd), interesting for its compression speed; it is best treated as an additional option, keeping Brotli and gzip.

Static and dynamic: different levels

The key point is to distinguish two cases.

Static files: compress once, at maximum

Bundled CSS and JavaScript, SVG icons and data files that change only with each release can be compressed in advance, during the build, at the maximum level. Compression time does not matter because you pay it only once. You generate .br and .gz files alongside the original, and the web server serves them directly when the client accepts them.

brotli -q 11 -k dist/app.js      # creates app.js.br
gzip -9 -k dist/app.js           # creates app.js.gz

On nginx, for example, the gzip_static directive and, with the dedicated module, brotli_static tell the server to look for the precompressed version before compressing on the fly.

Dynamic responses: a middle level

HTML generated on each request and API responses must be compressed on the fly. Using Brotli 11 here would be a mistake: compression takes longer than it saves in transfer, and TTFB gets worse. For dynamic content choose a medium level, such as Brotli 4-6 or gzip 5-6, which delivers much of the benefit at a modest CPU cost.

What not to compress

Some formats are already compressed internally and gain nothing from a second pass:

  • JPEG, PNG, WebP, AVIF and GIF images;
  • video and audio (MP4, WebM, MP3, AAC);
  • WOFF2 fonts, which already use Brotli internally;
  • ZIP archives and compressed PDFs.

Compressing them consumes CPU and sometimes produces slightly larger files. The correct configuration explicitly lists the text MIME types to compress: text/html, text/css, application/javascript, application/json, image/svg+xml, application/xml and the like. The old TTF font format also benefits from compression, while WOFF2 does not.

One more precaution: do not compress tiny responses, below a few hundred bytes, because the algorithm's overhead can cancel the gain.

Checking that it works

There are three simple ways:

  1. DevTools, Network panel: add the «Content-Encoding» column and compare transferred size with resource size;
  2. command line: explicitly request an encoding and read the headers;
curl -sI -H "Accept-Encoding: br" "$FILE_URL" | grep -i content-encoding
  1. cache check: verify that the delivery network, if there is one, does not decompress the file and send it uncompressed, which can happen with incorrect configurations.

A mistake you see often

Enabling compression on the origin server and also on the proxy or CDN can cause double compression or, more often, a cache that stores only one variant while ignoring Vary. The symptom is that some users receive unreadable or uncompressed files depending on which visitor first populated the cache. It is best to decide on a single point where dynamic compression happens and check its configuration.

In short

  • Text (HTML, CSS, JS, JSON, SVG) should always be compressed; images, video and WOFF2 should not.
  • Brotli normally compresses web text better than gzip; keep gzip as a fallback.
  • Static files: precompress at build time at the maximum level.
  • Dynamic responses: middle levels so as not to worsen TTFB.
  • Check Content-Encoding and Vary in DevTools or with curl.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. A computer science graduate, he combines rigorous technical analysis with attention to the people who use technology.

Read also