Skip to main content
All fixes

Page speed, redirects, Core Web Vitals

Enable Brotli or gzip to compress text assets

Serving HTML, CSS, and JS uncompressed wastes 60-80% of payload bytes. Enable Brotli on your CDN or origin to cut transfer size and speed up FCP and LCP.

What's happening

HTML, CSS, JavaScript, JSON, SVG, and font files all compress extremely well — typically 70-85% reduction with gzip and 80-90% with Brotli at level 11. When the origin server or CDN doesn't enable compression, every byte travels uncompressed across the network. The Lighthouse "Enable text compression" audit flags this with the exact bytes saved per asset.

Compression negotiation happens via the Accept-Encoding request header (br, gzip, deflate) and the Content-Encoding response header. Chrome DevTools Network panel shows the Content-Encoding column when enabled, and the Size column shows transferred-vs-decoded sizes for each asset. If text/css or application/javascript responses don't show br or gzip in Content-Encoding, compression is broken.

Most modern CDNs (Cloudflare, Fastly, Vercel, Netlify, CloudFront) enable Brotli automatically. The problem usually crops up on self-hosted nginx or Apache origins where the admin enabled gzip but never installed Brotli, or on edge functions returning custom Response objects without compressing the body.

Why it matters

Uncompressed assets directly slow FCP and LCP by inflating transfer time. A 200KB uncompressed JS bundle takes 2.5x longer to download on 4G mobile than its 65KB Brotli-compressed equivalent. Multiply across multiple assets and the cumulative delay can be a full second.

Beyond performance, uncompressed responses cost more bandwidth — both on your origin egress bill and on users' mobile data plans. CDN egress is often the largest line item in a small site's hosting cost; turning on Brotli halves it overnight.

Common causes

  • Self-hosted nginx without ngx_brotli compiled in.
  • Apache without mod_deflate or mod_brotli enabled.
  • Edge function or serverless route returning a Response without Content-Encoding.
  • Old CDN configurations that compress only specific MIME types and miss new ones.
  • Pre-compressed static assets served without correct Content-Encoding headers.
  • Reverse proxies between the CDN and origin stripping the Content-Encoding header.

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

    Verify Content-Encoding in DevTools

    Open DevTools Network panel, reload the page, click any HTML/CSS/JS asset, and check the Response Headers. Content-Encoding should be br for Brotli or gzip. If absent, compression is off. WebPageTest's content breakdown shows transferred vs decoded sizes per asset.

  2. 2

    Enable Brotli on your CDN

    Cloudflare, Fastly, Vercel, and Netlify enable Brotli with a checkbox or are on by default. CloudFront enables it via Behavior settings → Compress objects automatically (which uses gzip; for Brotli use a Lambda@Edge or a managed origin like S3 + Brotli pre-compressed).

  3. 3

    Configure nginx with ngx_brotli

    Compile nginx with the ngx_brotli module or install the binary build (nginx-extras on Debian). Enable in the server block: brotli on; brotli_comp_level 6; brotli_types text/css application/javascript application/json image/svg+xml. Restart nginx and verify with curl -H 'Accept-Encoding: br' -I.

  4. 4

    Pre-compress static assets at build time

    Generate.br and.gz versions of every static asset during build with brotli -k -q 11 file.js. Configure nginx with gzip_static on and brotli_static on so it serves the pre-compressed file when the client supports the encoding. Build-time Brotli at level 11 squeezes out 5-10% more than runtime level 4.

  5. 5

    Compress responses in edge functions

    Vercel Edge, Cloudflare Workers, and Fastly Compute@Edge automatically negotiate compression for text responses. Don't return new Response(JSON.stringify(...)) without setting Content-Type — the runtime needs the type to decide whether to compress.

  6. 6

    Don't compress already-compressed formats

    JPEG, PNG, WebP, AVIF, MP4, WOFF2, and ZIP are already compressed. Re-compressing them wastes CPU and adds a few bytes. Configure nginx's brotli_types and gzip_types to whitelist only text-like MIME types.

  7. 7

    Re-run Lighthouse to confirm savings

    After enabling compression, the "Enable text compression" audit should disappear. The Performance score typically rises 3-5 points and FCP drops by hundreds of ms on slow connections. Lighthouse CI catches regressions when a config change disables compression.

Example

# /etc/nginx/conf.d/compression.conf
brotli on;
brotli_static on;          # serve .br files when present
brotli_comp_level 6;       # runtime level (1-11)
brotli_min_length 256;
brotli_types
    text/plain
    text/css
    text/xml
    application/javascript
    application/json
    application/xml+rss
    application/atom+xml
    image/svg+xml
    font/woff
    font/woff2;

gzip on;
gzip_static on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types
    text/plain text/css text/xml
    application/javascript application/json application/xml+rss
    image/svg+xml font/woff font/woff2;

nginx Brotli + gzip with static pre-compression and explicit MIME whitelist.

Frequently asked

Yes for HTTPS connections. Brotli is supported in every evergreen browser since 2017 and only negotiated over HTTPS. The Accept-Encoding header advertises support — the server responds with Content-Encoding: br when both sides agree.

Level 4-6 for runtime compression (cheap fast 95% of the savings). Level 11 for build-time pre-compression (slow but maximum savings). Don't use level 11 at runtime — it can take 100ms+ per response.

Yes. Brotli is on by default for all Cloudflare-proxied origins on text responses. Verify with curl -H 'Accept-Encoding: br' -I https://yoursite.com.

Related fixes