How to Precompress Static Assets at Build Time
This guide is part of Compression: Brotli & Zstandard, within Network & Server Response Optimization. Static assets — hashed JavaScript and CSS bundles, SVGs, static HTML from site generators, JSON data files — are identical for every request until the next build. Compressing them on every request wastes CPU and forces a low compression level to keep latency acceptable. Compressing them once at build time lets you use the slowest, strongest settings: Brotli level 11 and gzip level 9 (or Zopfli), typically 5–10% smaller than on-the-fly levels, at zero runtime cost.
The build produces app.js, app.js.br and app.js.gz side by side. The server (or CDN) inspects Accept-Encoding and serves the matching precompressed file with the right Content-Encoding header. The pieces that go wrong are serving configuration (files exist but are not used), wrong headers (Content-Type set to application/x-brotli), and stale compressed files after a build change.
Rapid Diagnosis
- Check build output for
.br/.gzfiles next to assets. - Check response headers:
content-encoding: brand correctcontent-typefor JS and CSS. - Compare transfer size with a locally computed Brotli 11 size; a larger transfer means on-the-fly compression at a lower level.
- Check server CPU for compression time on static routes.
Root Cause Analysis
1. No precompression step. Assets are compressed per request by the server or CDN.
2. Precompressed files ignored. Server lacks brotli_static/gzip_static or equivalent.
3. CDN recompresses. The CDN compresses at the edge at its own level instead of using origin files.
4. Wrong headers. Compressed files served with incorrect Content-Type break loading.
Step-by-Step Resolution
1. Add a precompression step
Use a bundler plugin (for example vite-plugin-compression, compression-webpack-plugin) or a small script after the build.
// vite.config.js
import compression from 'vite-plugin-compression2';
export default {
plugins: [
compression({ algorithm: 'brotliCompress', include: /\.(js|css|html|svg|json|xml|txt|wasm)$/, threshold: 1024 }),
compression({ algorithm: 'gzip', include: /\.(js|css|html|svg|json|xml|txt|wasm)$/, threshold: 1024 }),
],
};
// trade-off: maximum Brotli adds build time (seconds per large bundle); cache
// build outputs so unchanged hashed files are not recompressed.
2. Serve precompressed files
location /assets/ {
brotli_static on; # serve .br when Accept-Encoding includes br
gzip_static on; # serve .gz otherwise
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary Accept-Encoding;
}
For Node.js, express-static-gzip or similar middleware serves precompressed variants. Static hosts (Netlify, Cloudflare Pages, Vercel) compress automatically; check whether they use your precompressed files or compress at the edge.
3. Upload to object storage correctly
When serving from S3-like storage behind a CDN, upload the compressed body under the original name with Content-Encoding: br, or use the CDN's encoding-aware variants. Never serve .br files with Content-Type: application/x-brotli.
4. Exclude already-compressed files
Skip images, fonts (WOFF2), video, audio and archives.
Verification
curl -s -H 'Accept-Encoding: br' -D - -o /tmp/a.br https://example.com/assets/app.4f2a.js | grep -iE 'content-(encoding|type|length)'
ls -l dist/assets/app.4f2a.js.br
# trade-off: the transferred Content-Length should equal the .br file size; a
# mismatch means something recompressed or decompressed it along the way.
Worked Example: A Static Documentation Site
A documentation site deployed 1,800 pages of static HTML plus assets to a self-managed nginx behind a CDN. nginx compressed with gzip level 6 per request; the CDN cached gzip variants. Adding Brotli 11 and Zopfli precompression to the build (adding 50 seconds to a 4-minute build) and enabling brotli_static/gzip_static reduced HTML transfer by 19% and JS by 17%. Origin CPU during cache refills dropped by 60%. LCP p75 improved by 90ms on mobile, mostly from smaller HTML.
Precompression on Static Hosts and CDNs
Platforms differ. Some static hosts compress at the edge automatically with Brotli at good levels; precompressing yourself may not change much. Others serve files as uploaded, so precompression is required for Brotli. Some CDNs ignore origin Content-Encoding and recompress. Test what users actually receive: compare the transferred size of a known asset against your locally computed Brotli 11 size. If they match, precompression is effective; if the transferred size is larger, something in the chain recompresses at a lower level.
Making Precompression Part of CI
Treat precompressed outputs as part of the build contract. A CI step can verify that every text asset above the size threshold has .br and .gz siblings, that each compressed file decompresses back to the original bytes, and that compressed sizes stay within budget. After deployment, a smoke test requests a few assets with Accept-Encoding: br and compares Content-Length with the size of the matching .br file in the build output. This catches the most common failure — compressed files present but not served — on the first deploy rather than months later.
Common Mistakes
- Precompressing but not serving the files. Server configuration missing.
- Wrong Content-Type on compressed files. Scripts and styles fail to load.
- Stale compressed files. Old
.brfiles served after source changes when names are not hashed. - Compressing tiny files. Overhead with no benefit; use a threshold.
Edge Cases
Unhashed files. For files like index.html or robots.txt, regenerate compressed variants on every build and avoid long cache lifetimes.
Source maps. Compress them too if served publicly; they are large text files.
WebAssembly. WASM compresses moderately with Brotli; include it.
Service worker precaching. Service workers store decompressed responses; precompression helps the network transfer, not cache storage.
FAQ
Why precompress instead of letting the server compress?
Precompression allows maximum compression levels at no runtime cost and removes CPU work from every request.
What is Zopfli?
A gzip-compatible compressor that produces smaller gzip files than standard gzip at the cost of much slower compression — suitable for build-time use.
Do I need both .br and .gz files?
Yes, for broad compatibility: Brotli for modern browsers, gzip for clients without Brotli support.
Does precompression work with CDNs?
It depends on the CDN. Some pass through origin-compressed responses; others recompress. Verify transferred sizes.
How much build time does Brotli 11 add?
Seconds for large bundles, less for small files. Most builds add well under a minute, and caching avoids recompressing unchanged hashed files.
Should I precompress HTML?
Static HTML from site generators, yes. Server-rendered HTML is generated per request and must be compressed on the fly.
Can I precompress with zstd as well?
Yes, if your server can serve .zst files to clients that accept zstd. For static assets, Brotli 11 usually produces the smallest files.
Does precompression change cache behaviour?
No. Responses are cached per encoding just as with on-the-fly compression; set Vary: Accept-Encoding and long-lived immutable caching on hashed files.
What about images embedded as base64 in CSS or JS?
Base64 data compresses poorly and inflates text files by about a third. Move large embedded images to separate files so they can be cached and served in efficient formats.
Is precompression useful for small sites?
Yes, if the host serves the files you upload. It is a one-time build change that makes every text download smaller and removes compression work from the server, regardless of traffic volume.
How do I precompress in a webpack build?
Add compression-webpack-plugin twice, once with the Brotli algorithm and once with gzip, filtered to text file extensions and a minimum size. The plugin writes compressed files next to the originals for the server to pick up.
Related
- Enabling Brotli on nginx and CDNs — serving configuration.
- Compression dictionary transport — beyond single-file compression.
- Advanced caching strategies & CDN architecture — hashed filenames and immutable caching that make precompression safe.