Gzip and Brotli Compression Setup: A Practical Guide for Modern Web Assets
Compressing text‑based resources (HTML, CSS, JavaScript, JSON, SVG) is one of the most effective ways to shrink page weight, improve Core Web Vitals, and reduce bandwidth costs. While Gzip has been the workhorse for years, Brotli now offers better ratios for static files and is supported by every major browser over HTTPS. This guide walks you through a complete, production‑ready setup for both algorithms, covering the most common stacks and providing verification commands you can run right away.
Why Use Both Gzip and Brotli?
- Brotli advantage – For CSS, HTML, and JavaScript, Brotli typically yields 15‑25 % smaller files than Gzip at comparable compression levels.
- Gzip fallback – Older browsers or certain proxies may not send
Accept-Encoding: br. Keeping Gzip enabled guarantees that every client receives a compressed response. - Pre‑compression vs. on‑the‑fly – Pre‑compressing assets at build time (producing
.gzand.brfiles) saves CPU on every request and lets you use the maximum compression levels (Gzip 9, Brotli 11). On‑the‑fly compression is still useful for dynamically generated responses (e.g., API JSON) where pre‑compression isn’t feasible.
A robust strategy therefore combines:
- Pre‑compressed static files served directly by the web server or CDN.
- On‑the‑fly compression for any remaining dynamic content.
- Proper cache‑key handling (
Accept-Encodingin the cache key) so compressed and uncompressed variants are cached separately.
1. Preparing Your Assets
Before any server configuration, generate the compressed versions of your static files. Most modern build tools have plugins for this; a quick command‑line approach works as well.
# Gzip (maximum compression)
gzip -k -9 app.js # produces app.js.gz
# Brotli (maximum compression)
brotli --best app.js # produces app.js.br
- Keep the original file alongside its compressed counterparts.
- For versioned or hashed filenames (e.g.,
app.a1b2c3.js), compress the hashed file; the hash ensures long‑term caching. - Set the correct metadata when uploading:
Content-Type→ MIME type of the original file (application/javascriptfor.js).Content-Encoding→gziporbrdepending on the file you are uploading.
2. Nginx (Static or Proxy)
Nginx can serve pre‑compressed files with the gzip_static and brotli_static modules, and it can also compress on the fly.
Enable the Modules
If you’re using a custom build, load the modules at the top of nginx.conf:
load_module modules/ngx_http_gzip_static_module.so;
load_module modules/ngx_http_brotli_static_module.so;
Core Configuration (inside http {} or a server {} block)
# Serve pre‑compressed files when they exist
gzip_static on;
brotli_static on;
# On‑the‑fly compression for anything not pre‑compressed
gzip on;
gzip_vary on;
gzip_min_length 256; # avoid compressing tiny responses
gzip_comp_level 6; # good balance CPU/size
gzip_types text/plain text/css application/json application/javascript text/xml application/xml+rss;
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml+rss;
Location‑Specific Tweaks
If you serve assets from a dedicated folder (e.g., /static/), you can tighten the settings:
location /static/ {
# Ensure static files get long cache lives when they are versioned
expires 1y;
add_header Cache-Control "public, immutable";
# Allow downstream caches to vary by encoding
add_header Vary Accept-Encoding;
}
Testing
# Gzip
curl -I -H "Accept-Encoding: gzip" https://example.com/static/app.js
# Look for: Content-Encoding: gzip
# Brotli
curl -I -H "Accept-Encoding: br" https://example.com/static/app.js
# Look for: Content-Encoding: br
A second request should show X-Cache: HIT if you have a caching layer in front.
3. Apache HTTP Server
Apache uses mod_deflate for on‑the‑fly Gzip and can be extended with mod_brotli for Brotli support.
Install Modules
On Debian/Ubuntu:
sudo a2enmod deflate
sudo a2enmod brotli # requires mod_brotli package
Configuration (.htaccess or virtual host)
<IfModule mod_deflate.c>
# Compress on the fly
AddOutputFilterByType DEFLATE text/plain text/css application/json application/javascript text/xml application/xml+rss
</IfModule>
<IfModule mod_brotli.c>
# Brotli compression (on the fly)
AddOutputFilterByType BROTLI_COMPRESS text/plain text/css application/json application/javascript text/xml application/xml+rss
BrotliCompressionQuality 6
</IfModule>
# Serve pre‑compressed files if they exist
<IfModule mod_headers.c>
RewriteEngine On
RewriteCond %{HTTP:Accept-Encoding} br
RewriteCond %{REQUEST_FILENAME}.br -f
RewriteRule ^(.*)$ $1.br [L]
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_FILENAME}.gz -f
RewriteRule ^(.*)$ $1.gz [L]
# Correct Content-Type and prevent double compression
RewriteRule "\.css\.gz$" - [T=text/css,E=no-gzip:1]
RewriteRule "\.js\.gz$" - [T=application/javascript,E=no-gzip:1]
RewriteRule "\.css\.br$" - [T=text/css,E=no-brotli:1]
RewriteRule "\.js\.br$" - [T=application/javascript,E=no-brotli:1]
</IfModule>
<FilesMatch "\.(css|js|html|xml|txt)$">
Header append Vary Accept-Encoding
</FilesMatch>
Verification
Same curl commands as for Nginx work; check the response headers for Content-Encoding: gzip or br.
4. ASP.NET Core (Kestrel, IIS, or behind a reverse proxy)
ASP.NET Core ships with response‑compression middleware that can handle both providers.
Register Services (Program.cs or Startup.cs)
builder.Services.AddResponseCompression(options =>
{
// Enable for HTTPS (required for Brotli in browsers)
options.EnableForHttps = true;
// Add both providers; order matters – Brotli tried first
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
// Optional: fine‑tune compression levels
builder.Services.Configure<BrotliCompressionProviderOptions>(opts =>
{
opts.Level = CompressionLevel.Fastest; // or SmallestSize for max ratio
});
builder.Services.Configure<GzipCompressionProviderOptions>(opts =>
{
opts.Level = CompressionLevel.SmallestSize;
});
Add Middleware
Place it before any middleware that writes the response (e.g., before UseStaticFiles if you want static files compressed on the fly, or after if you prefer pre‑compressed files served by the web server).
app.UseResponseCompression();
Serving Pre‑Compressed Static Files
If you prefer to host pre‑compressed files (recommended for SPAs), configure the static file provider to serve .gz and .br with correct headers:
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
var ext = Path.GetExtension(ctx.File.Name).ToLowerInvariant();
if (ext == ".gz")
{
ctx.Context.Response.Headers["Content-Encoding"] = "gzip";
}
else if (ext == ".br")
{
ctx.Context.Response.Headers["Content-Encoding"] = "br";
}
// Preserve original Content-Type
ctx.Context.Response.Headers["Content-Type"] =
MimeTypes.GetMimeType(Path.ChangeExtension(ctx.File.Name, null));
}
});
Testing
curl -I -H "Accept-Encoding: br" https://yourapp.com/css/site.css
# Expect Content-Encoding: br
5. Play Framework
Play’s built‑in gzip filter can be extended to Brotli via configuration.
application.conf
play.filters.gzip.enabled = true
play.filters.gzip.compression.brotli.enabled = true
play.filters.gzip.compression.level = 6 # adjust 1‑11 as needed
# Define which assets get compressed
play.filters.gzip.brotli.path = "/public/**"
play.filters.gzip.brotli.exclude = "/public/images/**"
Build Time Generation
Running sbt dist or sbt stage will automatically create .br files alongside the originals in the public/ folder.
Verification
Start the app and inspect a static asset:
curl -I -H "Accept-Encoding: br" http://localhost:9000/assets/main.js
# Look for Content-Encoding: br
If you sit behind Nginx or Apache, make sure those proxies also have brotli_static on; (or the Apache equivalent) so they don’t recompute the compression.
6. AWS S3 + CloudFront (Pre‑Compressed Assets)
When you cannot run compression logic at the origin (S3 is static object storage), pre‑compress at build time and let CloudFront serve the correct variant.
Step‑by‑Step Checklist
| Step | Action | Why |
|---|---|---|
| 1. Bucket policy | Allow s3:GetObject publicly or use Origin Access Control (OAC) for private buckets. | Guarantees CloudFront can read objects. |
| 2. Static website hosting | Enable only if you plan to serve directly from S3; otherwise skip. | Needed for direct‑S3 use cases. |
| 3. CORS | Add a CORS rule if assets are loaded cross‑origin (fonts, JSON). | Prevents browser blocking. |
4. Upload .gz files | aws s3 cp app.js.gz s3://mybucket/app.js --content-type application/javascript --content-encoding gzip --cache-control "public, max-age=31536000, immutable" | Pre‑compressed Gzip. |
5. Upload .br files | Same as above, but with --content-encoding br. | Pre‑compressed Brotli. |
| 6. CloudFront distribution | Point to the S3 REST endpoint (not the website endpoint) and attach OAC if used. | Allows secure origin access. |
| 7. Enable automatic compression | In the behavior settings, set “Compress objects automatically” to Yes. | Handles any dynamic or missed static files. |
| 8. Cache policy | Create a policy that includes Accept-Encoding in the cache key. | Guarantees separate caches for gzip, br, and uncompressed. |
| 9. Viewer protocol policy | Redirect HTTP → HTTPS. | Browsers only send Accept-Encoding: br over HTTPS. |
| 10. Response headers policy | Add Vary: Accept-Encoding. | Signals caches to store variants separately. |
| 11. Security headers | Attach a managed security headers policy (X‑Content‑Type‑Options, etc.). | Best practice. |
| 12. Verification | Use curl with the appropriate Accept-Encoding header and check for Content-Encoding and X-Cache. | Confirms everything works. |
Example Verification Commands
# Gzip
curl -I -H "Accept-Encoding: gzip" https://d111111abcdef8.cloudfront.net/app.js
# Should see: Content-Encoding: gzip
# and (on repeat) X-Cache: Hit from cloudfront
# Brotli
curl -I -H "Accept-Encoding: br" https://d111111abcdef8.cloudfront.net/app.js
# Should see: Content-Encoding: br
Why Pre‑Compress?
- S3 does not compress on the fly; the origin returns exactly what was stored.
- CloudFront’s on‑the‑fly compression is limited to level 6 (Gzip) and only for objects ≥1 KB.
- Pre‑compression lets you use level 9 (Gzip) and quality 11 (Brotli), yielding the smallest possible payloads while saving compute on every request.
7. WordPress
Many managed WordPress hosts already enable Brotli; if yours does not, you can activate it at the server level (Apache/Nginx) or via a plugin that injects the correct headers.
Using a Plugin (e.g., WP Rocket, W3 Total Cache)
- Enable Gzip compression (usually a toggle).
- Look for a Brotli option; if present, turn it on.
- Save and clear caches.
Manual Apache/Nginx
Follow the Apache or Nginx sections above, making sure the configuration applies to the wp-content/uploads, wp-includes, and wp-admin directories where CSS/JS live.
Verification
Run the same curl commands against a known CSS or JS file (e.g., https://example.com/wp-includes/css/dist/block-library/style.min.css). Check for Content-Encoding: br (preferred) or gzip.
8. Dockerized Angular (or any SPA) with Nginx
The pattern shown in the NgDocker article is reusable:
Dockerfile Highlights
# ---- Build stage ----
FROM node:18-alpine AS builder
WORKDIR /app
RUN apk add --no-cache brotli # ensure brotli binary
COPY package.json .
RUN npm install
COPY . .
RUN npm run build # produces /app/dist/<project>
# Compress everything in the output folder
RUN cd /app/dist && find . -type f -exec brotli --best {} \;
# ---- Runtime stage ----
FROM alpine:latest
RUN apk add --no-cache nginx nginx-mod-http-brotli
COPY nginx.conf /etc/nginx/http.d/default.conf
COPY --from=builder /app/dist/<project> /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf (minimal)
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
gzip_static on;
brotli_static on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript;
add_header Vary Accept-Encoding;
add_header Cache-Control "public, max-age=31536000, immutable";
}
Build & Run
docker build -t spa-demo .
docker run -p 8080:80 spa-demo
Test
curl -I -H "Accept-Encoding: br" http://localhost:8080/main.js
# Expect Content-Encoding: br
9. Verification Checklist (Universal)
Regardless of the platform, run these three checks after deployment:
- Header Presence
curl -I -H "Accept-Encoding: gzip" https://host/file.js
curl -I -H "Accept-Encoding: br" https://host/file.js
Verify Content-Encoding: gzip or br appears.
Make a first request (miss) then a second (hit). Look for X-Cache: Hit from cloudfront (CF) or X-Cache: HIT (NGINX/Varnish) and ensure the same encoding header is present on the hit.
- Cache Separation
- Transfer Size
curl -s -w "%{size_download}\n" -o /dev/null https://host/file.js # uncompressed size (if you request without Accept-Encoding)
curl -s -H "Accept-Encoding: br" -w "%{size_download}\n" -o /dev/null https://host/file.js
The Brotli request should show a substantially lower byte count (typically 15‑25 % less than the Gzip request).
If any of these checks fail, revisit the relevant configuration step (cache policy, Vary header, static‑file serving, or module activation).
10. Best‑Practice Summary
| Area | Recommendation |
|---|---|
| Asset preparation | Pre‑compress static files at build time with maximum levels (gzip -9, brotli --best). Keep originals for fallback. |
| Metadata | When uploading or serving .gz/.br, set Content-Type to the original MIME type and Content-Encoding to gzip or br. |
| Cache key | Always include Accept-Encoding in the cache key (CloudFront cache policy, Nginx gzip_static/brotli_static with add_header Vary Accept-Encoding;, Apache Header append Vary Accept-Encoding). |
| HTTPS requirement | Brotli is only negotiated over HTTPS; enforce redirect‑to‑HTTPS at the edge or load balancer. |
| Fallback chain | Try Brotli first, then Gzip, then raw. Most web servers and CDNs handle this automatically when both modules are active. |
| Monitoring | Track Content-Encoding headers in real‑user monitoring or synthetic tests; watch for sudden drops in compression ratio that may indicate misconfiguration. |
| Security | Pair compression with standard security headers (Strict‑Transport‑Security, X‑Content‑Type‑Options, X‑Frame‑Options). |
| Testing | Use the curl verification steps above; complement with browser DevTools → Network tab to see the content-encoding value on live pages. |
Conclusion
Setting up Gzip and Brotli compression is no longer an optional tweak—it’s a core part of delivering fast, efficient web experiences. By generating pre‑compressed assets, configuring your server or CDN to serve the correct variant based on the Accept-Encoding header, and verifying with simple curl commands, you guarantee that every visitor receives the smallest possible payload their browser can handle.
Whether you’re running a static site on S3 + CloudFront, a dynamic ASP.NET Core API, a Play Framework application, a Dockerized SPA, or a traditional WordPress blog, the patterns outlined above apply with only minor syntax changes. Implement them, measure the impact on page load times and Core Web Vitals, and enjoy the bandwidth savings and SEO benefits that come with a well‑compressed delivery pipeline. Happy compressing!
