Static asset optimisation: compression and modern images

Brotli and gzip negotiation, minification and bundling decisions, WebP and AVIF with fallbacks, responsive image markup, and content hashing for long-lived caches.

Compression

# Brotli when the client offers it, gzip as the fallback
brotli on;
brotli_comp_level 5;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;

gzip on;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_vary on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
  • Never compress an already-compressed format. Re-compressing a JPEG or a WebP wastes CPU and can make it larger.
  • Vary: Accept-Encoding is mandatory, or a proxy will serve a Brotli response to a client that cannot decode it.
  • Level 5 Brotli costs a fraction of level 11 and gets most of the benefit; build-time compression for static assets can afford level 11.
  • Pre-compress static assets at build time and serve them directly. Compression per request is CPU spent on something that never changes.
# what is actually being served, and how large it really is
curl -sI -H "Accept-Encoding: br, gzip" https://example.com/static/app.js \
  | grep -iE "content-encoding|content-length|vary|cache-control"

# measure the transfer size, not the decoded length
curl -s -o /dev/null -w "encoded bytes: %{size_download}\n" \
  -H "Accept-Encoding: br" https://example.com/static/app.js

Modern images

<!-- modern formats with an explicit fallback -->
<picture>
  <source type="image/avif" srcset="/img/hero-800.avif 800w, /img/hero-1600.avif 1600w"
          sizes="(max-width: 600px) 100vw, 50vw" />
  <source type="image/webp" srcset="/img/hero-800.webp 800w, /img/hero-1600.webp 1600w"
          sizes="(max-width: 600px) 100vw, 50vw" />
  <img src="/img/hero-800.jpg" width="1600" height="900" loading="lazy"
       decoding="async" alt="Dashboard overview" />
</picture>
FormatTypical saving versus JPEGUse it forWatch out for
WebP25-35 percentPhotographs and screenshotsVery wide browser support now
AVIF40-60 percentPhotographs, large hero imagesSlower to encode; support still growing
PNGNoneFlat colour, transparency, iconsEnormous for photographs
SVGN/AIcons and simple diagramsNot for photographs; can be large if unoptimised
JPEG XLComparable to AVIFNot yet a safe defaultBrowser support is limited
  1. Serve at the size it is displayed. A 2000 px image shown in a 400 px box wastes bandwidth on every visit.
  2. Always set width and height so the browser reserves space and the layout does not shift.
  3. Lazy load everything below the fold, and never the image that is the largest contentful paint.
  4. Optimise with a tool or a build step, not by re-exporting from a design tool at a guess.
  5. Strip metadata. Camera and editor metadata is often larger than the visible difference from removing it.

Hashing, minifying and bundling

Content-hashed filenames and a long lifetime

  /static/app.8f3c2a1b.js        Cache-Control: public, max-age=31536000, immutable
  /static/app.css.4d1e9f02.css   same
  /index.html                    Cache-Control: public, max-age=0, s-maxage=300

The rule: if the URL changes when the content changes, the URL can be cached
forever. If the URL is stable, the lifetime must be short - pick one, not both.
DecisionPreferReason
Bundle everything or split?Split by change frequencyA vendor bundle changes rarely and caches longer
Minify?Yes, in the buildSmaller payload with no runtime cost
Tree shake?YesUnused exports are the largest source of dead weight
Inline critical CSS?Yes, under about 14 KBRemoves a render-blocking request
Defer non-critical JS?YesParsing JavaScript blocks interaction
Keep source maps?Publicly reachable or omittedShip them privately for error tracking
⚠️
Changing the hash of a file that has not changed is as harmful as not hashing at all: it invalidates every user's cache on every deploy. Generate the hash from the content, never from the build timestamp or the commit id.

FAQ

Should compression happen at the CDN or the origin?
At the CDN when the response is dynamic, and at build time for static files. Compressing at the origin adds CPU to every request the edge has to forward.
How do I know which image format to choose?
Serve AVIF or WebP to clients that advertise it and keep a JPEG fallback. `` with a `` per format does this in markup with no server logic.

Image CDNs and on-the-fly transformation Cache headers and cache keys

Last refreshed 2026-09-18.