Skip to main content
All fixes

Page speed, redirects, Core Web Vitals

Enable Brotli compression for smaller text payloads

Brotli compresses HTML, CSS, and JS 15-25% smaller than gzip. Enable it on your CDN or origin to cut bytes over the wire and speed up FCP and LCP.

What's happening

Brotli is Google's compression algorithm, designed specifically for text-based web content. At equivalent CPU cost it produces 15-25% smaller output than gzip; at maximum quality (level 11) it can exceed 30% savings on some payloads. Every evergreen browser since 2017 supports it over HTTPS, advertised via Accept-Encoding: br.

When the browser sends Accept-Encoding: gzip, deflate, br and the server responds with Content-Encoding: gzip, you're leaving Brotli savings on the table. DevTools Network panel shows Content-Encoding per response in the Headers tab. Lighthouse's "Enable text compression" audit doesn't differentiate between gzip and Brotli, but the bytes-on-wire data in WebPageTest does.

Most modern CDNs (Cloudflare, Fastly, Vercel, Netlify) enable Brotli automatically. The problem usually crops up on self-hosted nginx without ngx_brotli compiled in, on Apache without mod_brotli, or on edge functions returning Response objects without Brotli compression configured.

Why it matters

Brotli's extra savings (15-25% on top of gzip) directly speed up FCP and LCP for text-heavy sites. A 200KB gzipped JS bundle becomes 160KB Brotli-compressed — that's 200ms saved on 4G mobile per asset.

Bandwidth cost is the secondary lever. Sites paying for CDN egress save 15-20% on text-asset bills overnight. For a site shipping 5TB/month of text traffic, that's a real number.

Common causes

  • Self-hosted nginx without ngx_brotli compiled or installed.
  • Apache without mod_brotli enabled (a2enmod brotli).
  • Edge function or serverless route returning Response without compression negotiation.
  • Pre-compressed static assets only providing.gz files, not.br.
  • Custom reverse proxy stripping br from Accept-Encoding before forwarding.
  • CDN configured to allow only gzip in cache layer.

Detect this on your site

Run a quick scan with the Speed Test. The tool surfaces this exact issue with the records and context needed to apply the fix below.

Open Speed Test

How to fix it

  1. 1

    Check Content-Encoding in DevTools

    Open DevTools Network panel, click an HTML/CSS/JS asset, look at Response Headers. Content-Encoding: br means Brotli; gzip means you're missing out. Run curl -H 'Accept-Encoding: br' -I https://yoursite.com to confirm from CLI.

  2. 2

    Enable Brotli on Cloudflare or Vercel

    Cloudflare: Speed → Optimization → Brotli (on by default). Vercel: Brotli is on by default for static and serverless responses. Netlify: same. If you're on these platforms, you probably already have Brotli.

  3. 3

    Install ngx_brotli on nginx

    On Debian/Ubuntu: apt install nginx-extras (which bundles ngx_brotli). On other distros, compile nginx with the module. Add to nginx.conf: brotli on; brotli_comp_level 6; brotli_types text/css application/javascript application/json image/svg+xml.

  4. 4

    Enable mod_brotli on Apache

    Run a2enmod brotli, then in your VirtualHost: AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json. Restart Apache. Verify with curl.

  5. 5

    Pre-compress static assets at build

    Generate.br files at build time with brotli -k -q 11 file.js. nginx with brotli_static on; serves them when the client sends Accept-Encoding: br. Build-time level 11 squeezes 5-10% more than runtime level 6.

  6. 6

    Configure edge function compression

    Cloudflare Workers, Vercel Edge, and Fastly Compute@Edge handle compression negotiation when you return Response with text Content-Type. For binary or custom encodings, manually compress with the CompressionStream API: new CompressionStream('br') wraps a stream.

  7. 7

    Don't compress already-compressed formats

    JPEG, PNG, WebP, AVIF, MP4, WOFF2, ZIP — already compressed. Re-compressing wastes CPU and grows bytes. Whitelist only text MIME types in nginx's brotli_types directive.

Example

# /etc/nginx/conf.d/brotli.conf
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;

http {
    brotli on;
    brotli_static on;        # serve pre-compressed .br files
    brotli_comp_level 6;     # 1-11; 6 is a good runtime balance
    brotli_min_length 256;
    brotli_types
        text/plain text/css text/xml
        application/javascript application/json application/xml
        application/atom+xml application/rss+xml
        image/svg+xml font/woff font/woff2;
}

# Build-time pre-compression script:
# find ./public -type f \( -name "*.js" -o -name "*.css" -o -name "*.html" \) \
#     -exec brotli -k -q 11 {} \;

nginx with Brotli runtime compression plus build-time level-11 static.

Frequently asked

Runtime: level 4-6 (cheap 95% of the savings). Build-time pre-compression: level 11 (slow but maximum savings paid once at build). Level 11 at runtime adds 100ms+ per response — don't.

Brotli works over HTTP technically but browsers only negotiate it over HTTPS for security reasons. If you're on HTTP you're missing more than Brotli — you're missing HTTP/2 HTTP/3 and basic security.

No. Keep both. Browsers send Accept-Encoding: gzip deflate br and the server picks the best supported. Older clients fall back to gzip; modern ones get Brotli.

Related fixes