OneWebDesk

Enabling gzip and Brotli: Cut Transfer Size by 70%

What compressing text resources buys you, nginx/Apache config, verification and common mistakes.

HTTP compression is the highest-leverage performance change you can make with a few lines of server config: it typically shrinks text transfer size by 70–80%. A 238KB HTML document becomes roughly 30KB over gzip, pages paint sooner, and mobile users burn less data. CDNs usually enable it automatically, but on self-managed nginx or Apache servers it is surprisingly common to find compression missing entirely or only half-configured.

This guide covers what compression should (and should not) apply to, how the browser and server negotiate it, copy-paste config blocks for nginx and Apache, build-time precompression with Brotli, and the classic mistakes — double-compressing images, missing Vary, cranking the level too high. To see whether your site compresses today, drop a URL into the compression checker.

What to compress — text only, never images

General-purpose compressors like gzip and Brotli thrive on repetitive text. HTML tags, CSS property names, JS identifiers, and JSON keys repeat constantly, which is why text shrinks by 70–80%. Formats such as JPG, PNG, WebP, MP4, and WOFF2 are already compressed by design: re-compressing them saves 1–2% at best, sometimes makes files bigger, and always wastes CPU.

ResourceCompress?Why / typical savings
HTML, CSS, JSYesHighly repetitive — 70–80% smaller
JSON, XML, SVGYesAPI payloads and icons — 60–85% smaller
.txt, RSS feeds, source mapsYesAnything text-based qualifies
JPG, PNG, WebP, AVIFNoAlready compressed — near-zero gain, pure CPU cost
MP4, WOFF2, ZIP, most PDFsNoInternally compressed — double compression is pointless

To estimate what compression will buy you, check how much of your page is text in the first place: the page weight analyzer breaks total bytes down by resource type.

How negotiation works — Accept-Encoding and Content-Encoding

Compression is negotiated automatically on every request. The handshake is simple:

  1. The browser sends Accept-Encoding: gzip, deflate, br, zstd, declaring which formats it can decode. Every modern browser supports gzip and Brotli (br).
  2. The server picks a format it supports, compresses the body, and labels the response with Content-Encoding: gzip (or br).
  3. The browser decompresses transparently before rendering. Nothing is required on the client side.

One more header matters: because the same URL now has compressed and uncompressed variants, responses must carry Vary: Accept-Encoding so intermediate caches (CDNs, proxies) store the variants separately. Without it, a cache can hand a gzipped body to a client that never asked for gzip.

nginx config — worked example: 238KB HTML to 30KB

gzip ships built into nginx. Add this inside the http block:

# /etc/nginx/nginx.conf — inside http { }
gzip on;
gzip_comp_level 5;           # 1–9; 5 is the speed/ratio sweet spot
gzip_min_length 256;         # skip tiny responses
gzip_vary on;                # adds Vary: Accept-Encoding
gzip_proxied any;            # compress behind proxies too
gzip_types
  text/css text/plain text/xml
  application/javascript application/json
  application/xml application/rss+xml
  image/svg+xml font/ttf;    # text/html is implicit — listing it warns

With exactly this config, a 238,412-byte HTML document comes back with Content-Encoding: gzip at about 30KB — an 87% reduction. On a 5Mbps mobile link that cuts transfer time from roughly 380ms to 48ms. Measure the before/after yourself with the response time tool.

Brotli squeezes the same text 15–20% smaller than gzip. It is not in stock nginx — you build in the ngx_brotlimodule — and the directives mirror gzip almost exactly:

# after installing ngx_brotli
brotli on;
brotli_comp_level 5;         # 4–6 for on-the-fly; save 11 for static files
brotli_types text/css application/javascript application/json image/svg+xml;

If building a module is more trouble than it is worth, putting a CDN like Cloudflare in front gets you Brotli automatically; the origin only needs gzip.

Apache, and precompressing at build time (.br/.gz)

Apache handles this with mod_deflate:

# .htaccess or VirtualHost
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/css text/plain
  AddOutputFilterByType DEFLATE application/javascript application/json
  AddOutputFilterByType DEFLATE application/xml image/svg+xml
</IfModule>

The next level up is static precompression: generate app.js.br and app.js.gz next to your bundles at build time, and let the server hand the ready-made file over without compressing anything per request. Two wins:

  • Zero request-time CPU— traffic spikes cost the server nothing extra.
  • Maximum ratios— since it runs offline, you can afford Brotli level 11 and gzip level 9. Brotli 11 typically beats on-the-fly compression by another 10%+.

nginx serves precompressed files automatically via gzip_static on; (built in) and brotli_static on; (ngx_brotli). Vite, webpack, and Next.js all have plugins that emit .br/.gz during the build.

Verifying it works, and five pitfalls

After deploying, verify with the compression checker or by sending the header yourself with curl:

curl -sI -H "Accept-Encoding: gzip, br" https://example.com/ | grep -i \
  -e content-encoding -e vary
# content-encoding: br   ← compression active
# vary: Accept-Encoding  ← cache-safe

Watch out for these traps:

  • Adding images or video to gzip_types: zero savings, per-request CPU burned for nothing. List text MIME types only.
  • Compression level too high: gzip 9 costs several times the CPU of level 5 for a 1–2 percentage-point better ratio. Use gzip 5 / Brotli 4–6 for dynamic responses; reserve the maximum levels for build-time precompression.
  • Missing Vary: Accept-Encoding: the classic cause of caches serving the wrong variant. In nginx a single gzip_vary on; fixes it.
  • Compressing tiny responses: below a few hundred bytes, gzip framing overhead can make output larger. Keep a floor like gzip_min_length 256.
  • BREACH: if a dynamic response mixes a secret (e.g. a CSRF token) with attacker-reflected input, observing compressed sizes can leak the secret. Disable compression for those specific responses or use per-request token masking. Static assets and public pages are not affected.

Finally, quantify the win: record transfer bytes before and after with the page weight analyzer and latency with the response time tool— a before/after number is the easiest performance argument you will ever make.

Frequently asked questions

Should I use gzip or Brotli?
Both. The server simply serves Brotli (15-20% smaller than gzip on text) to browsers that advertise br in Accept-Encoding and falls back to gzip otherwise. If building the nginx Brotli module is a hassle, gzip alone still saves 70%+ — enable gzip at the origin and let a CDN add Brotli on top.
Should images like JPG, PNG, or WebP be gzipped too?
No. Images, video, and WOFF2 fonts are already compressed internally; running gzip over them saves 1-2% at best while burning CPU on every request. Shrink images by converting formats (WebP/AVIF) and resizing, not with transfer encoding.
How do I check whether compression is enabled?
Look for Content-Encoding: gzip or br in the response headers. Run curl -sI -H "Accept-Encoding: gzip, br" against the URL, or use the compression checker, which reports the encoding and the savings percentage in one shot. Note that servers only compress when the request includes Accept-Encoding, so a plain curl without that header will always show an uncompressed response.
What compression level should I set?
For on-the-fly compression, gzip level 5 (and Brotli 4-6) is the sweet spot; level 9 costs several times more CPU for only 1-2 percentage points of extra savings. Maximum levels — gzip 9, Brotli 11 — belong in build-time precompression, where files are compressed once and served millions of times.
Is HTTP compression a security risk?
Only in one specific scenario. The BREACH attack works when a compressed dynamic response contains both a secret (like a CSRF token) and attacker-controllable reflected input. Disable compression on those responses or use token masking, which most modern frameworks provide. Static assets and public pages without secrets are safe to compress.

Tools to use with this guide

Related guides