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.
| Resource | Compress? | Why / typical savings |
|---|---|---|
| HTML, CSS, JS | Yes | Highly repetitive — 70–80% smaller |
| JSON, XML, SVG | Yes | API payloads and icons — 60–85% smaller |
.txt, RSS feeds, source maps | Yes | Anything text-based qualifies |
| JPG, PNG, WebP, AVIF | No | Already compressed — near-zero gain, pure CPU cost |
| MP4, WOFF2, ZIP, most PDFs | No | Internally 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:
- The browser sends
Accept-Encoding: gzip, deflate, br, zstd, declaring which formats it can decode. Every modern browser supports gzip and Brotli (br). - The server picks a format it supports, compresses the body, and labels the response with
Content-Encoding: gzip(orbr). - 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 warnsWith 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 singlegzip_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?
Should images like JPG, PNG, or WebP be gzipped too?
How do I check whether compression is enabled?
What compression level should I set?
Is HTTP compression a security risk?
Tools to use with this guide
Related guides
- Cache-Control Done Right: Making Browser Caching WorkWhat max-age, no-cache, no-store and immutable really mean, with recommended policies per resource type.
- Slow Site? Diagnosing and Fixing TTFB Step by StepWhere time-to-first-byte goes — DNS, TLS, server, database — and how to diagnose and cut each stage.
- Core Web Vitals (LCP, CLS, INP) Improvement ChecklistWhat LCP, CLS and INP measure, and a practical checklist to improve each score.