Gzip and Brotli Compression Setup: A Practical Guide for Modern Web Assets

gzip
brotli
compression
web performance
CDN

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 .gz and .br files) 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:

  1. Pre‑compressed static files served directly by the web server or CDN.
  2. On‑the‑fly compression for any remaining dynamic content.
  3. Proper cache‑key handling (Accept-Encoding in 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/javascript for .js).
  • Content-Encodinggzip or br depending 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

StepActionWhy
1. Bucket policyAllow s3:GetObject publicly or use Origin Access Control (OAC) for private buckets.Guarantees CloudFront can read objects.
2. Static website hostingEnable only if you plan to serve directly from S3; otherwise skip.Needed for direct‑S3 use cases.
3. CORSAdd a CORS rule if assets are loaded cross‑origin (fonts, JSON).Prevents browser blocking.
4. Upload .gz filesaws 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 filesSame as above, but with --content-encoding br.Pre‑compressed Brotli.
6. CloudFront distributionPoint to the S3 REST endpoint (not the website endpoint) and attach OAC if used.Allows secure origin access.
7. Enable automatic compressionIn the behavior settings, set “Compress objects automatically” to Yes.Handles any dynamic or missed static files.
8. Cache policyCreate a policy that includes Accept-Encoding in the cache key.Guarantees separate caches for gzip, br, and uncompressed.
9. Viewer protocol policyRedirect HTTP → HTTPS.Browsers only send Accept-Encoding: br over HTTPS.
10. Response headers policyAdd Vary: Accept-Encoding.Signals caches to store variants separately.
11. Security headersAttach a managed security headers policy (X‑Content‑Type‑Options, etc.).Best practice.
12. VerificationUse 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)

  1. Enable Gzip compression (usually a toggle).
  2. Look for a Brotli option; if present, turn it on.
  3. 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:

  1. 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.

  1. Cache Separation
  1. 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

AreaRecommendation
Asset preparationPre‑compress static files at build time with maximum levels (gzip -9, brotli --best). Keep originals for fallback.
MetadataWhen uploading or serving .gz/.br, set Content-Type to the original MIME type and Content-Encoding to gzip or br.
Cache keyAlways 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 requirementBrotli is only negotiated over HTTPS; enforce redirect‑to‑HTTPS at the edge or load balancer.
Fallback chainTry Brotli first, then Gzip, then raw. Most web servers and CDNs handle this automatically when both modules are active.
MonitoringTrack Content-Encoding headers in real‑user monitoring or synthetic tests; watch for sudden drops in compression ratio that may indicate misconfiguration.
SecurityPair compression with standard security headers (Strict‑Transport‑Security, X‑Content‑Type‑Options, X‑Frame‑Options).
TestingUse 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!

Share this post:
Gzip and Brotli Compression Setup: A Practical Guide for Modern Web Assets