HTTP Compression in .htaccess
Every kilobyte you send over the network costs time. On a 3G mobile connection, an uncompressed 200KB JavaScript file takes noticeably longer to download than a compressed 50KB version of the same file. HTTP compression reduces the size of text-based responses before they leave your server, often achieving 60-80% size reduction with negligible CPU overhead. It is one of the highest-impact performance optimizations you can make, and it requires no changes to your application code.
This guide covers why compression matters, how to fix broken encoding headers from misbehaving proxies, how to configure both gzip (mod_deflate) and Brotli (mod_brotli) compression, which file types benefit from compression, which should be left alone, and how to verify that compression is working correctly.
Why Compression Matters
When a browser requests a resource from your server, it sends an Accept-Encoding header indicating which compression algorithms it supports:
Accept-Encoding: gzip, deflate, br
If the server supports one of these algorithms and the response is compressible, it compresses the response body and sends it with a Content-Encoding header:
Content-Encoding: gzip
The browser decompresses the content transparently. The user sees no difference except that the page loads faster.
Real-World Impact
| Resource Type | Uncompressed | Gzip Compressed | Brotli Compressed | Savings |
|---|---|---|---|---|
| HTML page | 100 KB | 25 KB | 22 KB | 75-78% |
| CSS file | 150 KB | 30 KB | 25 KB | 80-83% |
| JavaScript bundle | 500 KB | 120 KB | 100 KB | 76-80% |
| JSON API response | 80 KB | 15 KB | 12 KB | 81-85% |
| SVG image | 60 KB | 12 KB | 10 KB | 80-83% |
These savings translate directly to:
- Faster page loads: Less data to transfer means pages render sooner, especially on slow connections.
- Lower bandwidth costs: If you pay for bandwidth (CDN, cloud hosting), compression reduces your bill proportionally.
- Better Core Web Vitals: Google's Largest Contentful Paint (LCP) and other performance metrics improve with smaller transfer sizes.
- Improved mobile experience: Mobile connections are often bandwidth-constrained. Compression makes a dramatic difference on 3G/4G networks.
- Higher search rankings: Page speed is a Google ranking factor. Compression is explicitly recommended in Google's PageSpeed Insights.
Fixing Broken Accept-Encoding Headers
Before configuring compression, there is a practical problem to address. Some network proxies, corporate firewalls, antivirus software, and security appliances mangle or strip the Accept-Encoding header from HTTP requests. They do this to scan the response body for malware or to comply with content inspection policies, but the result is that your server never sees a valid Accept-Encoding header and therefore never compresses the response.
These broken headers look like:
Accept-EncodXng: gzip, deflate
X-cept-Encoding: gzip, deflate
~~~~~~~~~~~~~~~: gzip, deflate
---------------: gzip, deflate
XXXXXXXXXXXXXXX: gzip, deflate
The software modifies the header name just enough to make it unrecognizable to the server while keeping the value intact (hoping to restore it later). The fix detects these mangled headers and restores a proper Accept-Encoding header:
<IfModule mod_deflate.c>
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
</IfModule>
</IfModule>
</IfModule>
How this works:
SetEnvIfNoCasechecks if any incoming header matches the known mangled patterns (Accept-EncodXng,X-cept-Encoding, or a string of 15 repeated characters likeX,~, or-). It also checks that the value looks like a valid encoding list (gzip,deflate, or a string of the characters used for mangling).- If a match is found, the
HAVE_Accept-Encodingenvironment variable is set. RequestHeader appendadds a properAccept-Encoding: gzip,deflateheader to the request, but only if the environment variable is set (env=HAVE_Accept-Encoding).
This fix ensures that users behind problematic proxies still receive compressed responses. Place it before your compression configuration.
This fix addresses a relatively uncommon scenario. Most modern networks pass the Accept-Encoding header through unchanged. However, corporate environments and some mobile carriers still use intercepting proxies, and including this fix costs nothing while improving compression coverage for affected users.
mod_deflate Configuration
Apache's mod_deflate module provides gzip compression (despite the somewhat confusing name, mod_deflate actually uses gzip, not the raw deflate algorithm). It is the most widely supported and most commonly used compression module.
AddOutputFilterByType
The AddOutputFilterByType directive tells Apache to compress responses based on their media type (MIME type). This is the recommended approach because it targets specific content types rather than file extensions.
<IfModule mod_deflate.c>
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE "application/atom+xml" \
"application/javascript" \
"application/json" \
"application/ld+json" \
"application/manifest+json" \
"application/rdf+xml" \
"application/rss+xml" \
"application/schema+json" \
"application/geo+json" \
"application/vnd.ms-fontobject" \
"application/wasm" \
"application/x-font-ttf" \
"application/x-javascript" \
"application/x-web-app-manifest+json" \
"application/xhtml+xml" \
"application/xml" \
"font/eot" \
"font/opentype" \
"font/otf" \
"font/ttf" \
"image/bmp" \
"image/svg+xml" \
"image/vnd.microsoft.icon" \
"text/cache-manifest" \
"text/calendar" \
"text/css" \
"text/html" \
"text/javascript" \
"text/plain" \
"text/markdown" \
"text/vcard" \
"text/vnd.rim.location.xloc" \
"text/vtt" \
"text/x-component" \
"text/x-cross-domain-policy" \
"text/xml"
</IfModule>
</IfModule>
The <IfModule mod_filter.c> wrapper is required because AddOutputFilterByType relies on mod_filter in Apache 2.4+.
By Extension vs By Media Type
You might wonder why we use media types instead of file extensions. Both approaches work, but media types are more reliable:
By media type (recommended):
AddOutputFilterByType DEFLATE "text/html" "text/css" "text/javascript"
This compresses the response based on the actual Content-Type header, regardless of the file extension. A PHP script that outputs JSON with Content-Type: application/json is correctly compressed even though its file extension is .php.
By extension (less reliable):
<FilesMatch "\.(html|css|js)$">
SetOutputFilter DEFLATE
</FilesMatch>
This approach only looks at the file extension. Dynamic content generated by PHP, Python, or other languages would not be compressed unless you also match their extensions, and even then, the compression applies regardless of what content type the script actually outputs.
Always use AddOutputFilterByType for compression. It correctly handles dynamic content, works with any file extension, and ensures that only appropriate content types are compressed. The extension-based approach is a legacy pattern that should be avoided.
Setting Compression Level
You can control how aggressively mod_deflate compresses content:
<IfModule mod_deflate.c>
DeflateCompressionLevel 6
</IfModule>
The level ranges from 1 (fastest, least compression) to 9 (slowest, most compression). The default is typically 6, which provides a good balance. Going beyond 6 yields diminishing returns in file size reduction while significantly increasing CPU usage:
| Level | Compression Ratio | CPU Usage | Recommendation |
|---|---|---|---|
| 1 | Low | Very low | High-traffic sites with CPU constraints |
| 4-6 | Good | Moderate | Recommended for most sites |
| 9 | Slightly better | High | Rarely worth the CPU cost |
Brotli via mod_brotli
Brotli is a newer compression algorithm developed by Google that achieves 15-25% better compression than gzip for most web content. It is supported by all modern browsers (Chrome, Firefox, Safari, Edge) and is increasingly the preferred compression method.
Brotli support requires mod_brotli, which is available in Apache 2.4.26 and later.
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS "application/atom+xml" \
"application/javascript" \
"application/json" \
"application/ld+json" \
"application/manifest+json" \
"application/rdf+xml" \
"application/rss+xml" \
"application/schema+json" \
"application/geo+json" \
"application/vnd.ms-fontobject" \
"application/wasm" \
"application/xhtml+xml" \
"application/xml" \
"font/eot" \
"font/opentype" \
"font/otf" \
"font/ttf" \
"image/bmp" \
"image/svg+xml" \
"image/vnd.microsoft.icon" \
"text/cache-manifest" \
"text/calendar" \
"text/css" \
"text/html" \
"text/javascript" \
"text/plain" \
"text/markdown" \
"text/vcard" \
"text/vtt" \
"text/x-component" \
"text/xml"
</IfModule>
Using Both Brotli and Gzip
The best practice is to configure both algorithms. Apache automatically selects the best one based on what the browser supports. Browsers that support Brotli receive Brotli-compressed responses. Older browsers that only support gzip receive gzip-compressed responses.
# Brotli compression (preferred, better ratio)
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS "text/html" \
"text/css" \
"text/javascript" \
"application/javascript" \
"application/json" \
"application/xml" \
"image/svg+xml" \
"font/ttf" \
"font/otf" \
"font/woff" \
"application/vnd.ms-fontobject"
</IfModule>
# Gzip compression (fallback for older browsers)
<IfModule mod_deflate.c>
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE "text/html" \
"text/css" \
"text/javascript" \
"application/javascript" \
"application/json" \
"application/xml" \
"image/svg+xml" \
"font/ttf" \
"font/otf" \
"font/woff" \
"application/vnd.ms-fontobject"
</IfModule>
</IfModule>
When both modules are active, Apache checks the Accept-Encoding header:
- If the browser sends
Accept-Encoding: br, gzip, Apache uses Brotli. - If the browser sends
Accept-Encoding: gzip, Apache uses gzip. - If the browser sends no encoding support, Apache sends the response uncompressed.
mod_brotli is not available on all hosting providers. Many shared hosting environments only offer mod_deflate. If Brotli is not available, gzip alone is still a major improvement. You can check with apachectl -M | grep brotli or by examining phpinfo() output.
What to Compress
Not all content benefits equally from compression. The key distinction is between text-based formats (which compress extremely well) and binary formats (which are usually already compressed).
Text-Based Assets
These are your primary compression targets. Text-based files are highly repetitive and compress dramatically:
| Content Type | File Types | Typical Compression Ratio |
|---|---|---|
text/html | .html, .htm | 70-80% |
text/css | .css | 75-85% |
text/javascript | .js, .mjs | 70-80% |
text/plain | .txt, .log | 60-75% |
text/markdown | .md | 60-75% |
text/vtt | .vtt (subtitles) | 70-80% |
These are the highest-impact types to compress because they are both large and highly compressible.
SVG and XML
SVG images and XML documents are text-based and compress exceptionally well:
image/svg+xml → SVG images (often 80%+ compression)
application/xml → XML documents
application/atom+xml → Atom feeds
application/rss+xml → RSS feeds
application/xhtml+xml → XHTML pages
application/rdf+xml → RDF data
A complex SVG illustration might be 100KB uncompressed and 15KB after gzip. Since SVGs are increasingly used for icons, logos, and illustrations, compressing them provides significant savings.
JSON and API Responses
API responses are often the largest single transfers on modern web applications. JSON compresses extremely well due to its repetitive key structure:
application/json → Standard JSON
application/ld+json → JSON-LD (structured data)
application/manifest+json → Web app manifests
application/geo+json → Geographic data
application/schema+json → JSON Schema
A JSON API response with 1000 records might be 500KB uncompressed but only 50KB after compression. For data-heavy applications, compressing JSON responses is one of the most impactful optimizations you can make.
Fonts
Some font formats are uncompressed and benefit from gzip:
| Font Format | Compress? | Reason |
|---|---|---|
| TTF/OTF | Yes | Uncompressed by default. Significant savings. |
| EOT | Yes | Older format, typically uncompressed. |
| WOFF | Minimal | Already uses its own compression. Minor benefit. |
| WOFF2 | No | Already Brotli-compressed internally. |
What Not to Compress
Attempting to compress already-compressed content wastes CPU cycles, adds latency, and can occasionally make the output larger than the input. These formats should be excluded from compression.
Images (JPEG, PNG, WebP)
JPEG, PNG, WebP, AVIF, and GIF images use their own compression algorithms. Applying gzip on top provides negligible size reduction (often less than 1%) while consuming CPU:
image/jpeg → Already JPEG-compressed
image/png → Already uses DEFLATE internally
image/webp → Already compressed
image/avif → Already compressed
image/gif → Already uses LZW compression
Do not add image/jpeg, image/png, image/webp, image/avif, or image/gif to your AddOutputFilterByType list. Compressing these formats wastes server resources and provides no benefit. The only image format that should be compressed is SVG (image/svg+xml) because it is text-based XML.
Video and Audio
Media files are heavily compressed by their codecs. Additional compression is pointless:
video/mp4 → H.264/H.265 compressed
video/webm → VP8/VP9/AV1 compressed
audio/mpeg → MP3 compressed
audio/ogg → Vorbis/Opus compressed
audio/mp4 → AAC compressed
Already-Compressed Formats
Several other formats are internally compressed and should not be double-compressed:
| Format | Media Type | Why Not Compress |
|---|---|---|
| ZIP archives | application/zip | Already compressed with DEFLATE |
| GZIP archives | application/gzip | Already gzip-compressed |
| PDF files | application/pdf | Usually internally compressed (streams) |
| WOFF2 fonts | font/woff2 | Already Brotli-compressed |
| WOFF fonts | font/woff | Already compressed (minimal benefit from gzip) |
Common Mistake: Compressing Everything
Wrong approach:
# This compresses ALL responses, including images and video
SetOutputFilter DEFLATE
This applies compression globally to every response regardless of content type. Binary files like images and videos waste CPU cycles with zero benefit, and the output can occasionally be larger than the input.
Correct approach:
# Only compress content types that benefit from compression
<IfModule mod_deflate.c>
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE "text/html" "text/css" "text/javascript" "application/json" "image/svg+xml"
</IfModule>
</IfModule>
Explicitly list each content type that benefits from compression. If a type is not in the list, it passes through uncompressed.
Verifying Compression Is Working
After configuring compression, you need to verify that it is actually active. There are several ways to check.
Using curl
The most reliable method is to request a resource with curl and examine the response headers:
curl -I -H "Accept-Encoding: gzip, deflate, br" https://example.com/
Look for the Content-Encoding header in the response:
HTTP/2 200
content-type: text/html; charset=utf-8
content-encoding: gzip
vary: Accept-Encoding
If Content-Encoding: gzip (or br for Brotli) is present, compression is working.
To see the actual size difference, compare compressed and uncompressed responses:
# Compressed size
curl -so /dev/null -w '%{size_download}' -H "Accept-Encoding: gzip" https://example.com/
# Uncompressed size
curl -so /dev/null -w '%{size_download}' --compressed https://example.com/
Using Browser Developer Tools
- Open Developer Tools (F12 or Ctrl+Shift+I).
- Go to the Network tab.
- Reload the page.
- Click on any resource.
- In the Response Headers section, look for
Content-Encoding: gziporContent-Encoding: br.
Most browser Network tabs also show two size columns:
- Size: The compressed (transferred) size.
- Actual Size (or Content): The uncompressed (original) size.
If these two numbers differ significantly, compression is working.
Using Online Tools
Several online tools test compression:
- GTmetrix includes compression checks in its performance report.
- Google PageSpeed Insights flags uncompressed text resources as an optimization opportunity.
- GiftOfSpeed GZIP Test tests a specific URL for gzip compression.
Checking the Vary Header
When compression is active, Apache should include a Vary: Accept-Encoding header in the response:
Vary: Accept-Encoding
This header tells caches (CDNs, proxies, browsers) that the response content varies based on the Accept-Encoding request header. Without it, a cache might serve a gzip-compressed response to a client that does not support gzip, or an uncompressed response to a client that does support it.
If your Vary header is missing, add it explicitly:
<IfModule mod_headers.c>
<FilesMatch "\.(html|css|js|json|xml|svg|txt|vtt|md)$">
Header append Vary "Accept-Encoding"
</FilesMatch>
</IfModule>
Complete Production Configuration
Here is a comprehensive, production-ready compression configuration:
# === Fix Broken Accept-Encoding Headers ===
<IfModule mod_deflate.c>
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
</IfModule>
</IfModule>
</IfModule>
# === Brotli Compression (preferred) ===
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS "application/atom+xml" \
"application/javascript" \
"application/json" \
"application/ld+json" \
"application/manifest+json" \
"application/rdf+xml" \
"application/rss+xml" \
"application/schema+json" \
"application/geo+json" \
"application/vnd.ms-fontobject" \
"application/wasm" \
"application/xhtml+xml" \
"application/xml" \
"font/eot" \
"font/opentype" \
"font/otf" \
"font/ttf" \
"image/bmp" \
"image/svg+xml" \
"image/vnd.microsoft.icon" \
"text/cache-manifest" \
"text/calendar" \
"text/css" \
"text/html" \
"text/javascript" \
"text/plain" \
"text/markdown" \
"text/vcard" \
"text/vtt" \
"text/x-component" \
"text/xml"
</IfModule>
# === Gzip Compression (fallback) ===
<IfModule mod_deflate.c>
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE "application/atom+xml" \
"application/javascript" \
"application/json" \
"application/ld+json" \
"application/manifest+json" \
"application/rdf+xml" \
"application/rss+xml" \
"application/schema+json" \
"application/geo+json" \
"application/vnd.ms-fontobject" \
"application/wasm" \
"application/x-font-ttf" \
"application/x-javascript" \
"application/x-web-app-manifest+json" \
"application/xhtml+xml" \
"application/xml" \
"font/eot" \
"font/opentype" \
"font/otf" \
"font/ttf" \
"image/bmp" \
"image/svg+xml" \
"image/vnd.microsoft.icon" \
"text/cache-manifest" \
"text/calendar" \
"text/css" \
"text/html" \
"text/javascript" \
"text/plain" \
"text/markdown" \
"text/vcard" \
"text/vnd.rim.location.xloc" \
"text/vtt" \
"text/x-component" \
"text/x-cross-domain-policy" \
"text/xml"
</IfModule>
</IfModule>
HTTP compression is one of those rare optimizations that is all upside. It reduces transfer sizes by 60-80% for text-based content, speeds up page loads on every connection type, lowers bandwidth costs, and improves search engine rankings. Configure mod_deflate as a baseline, add mod_brotli if your server supports it, compress text-based formats only, and verify with curl or browser developer tools. The few minutes spent setting this up pay dividends on every single page load your server handles.