A Closer Look at Brotli Encoding for Static Assets

Lossless compression is a core part of optimizing any web service, and the choice of algorithm significantly impacts delivery efficiency. While gzip (based on DEFLATE) remains the industry standard for HTTP transport, the newer Brotli format offers substantially better compression ratios for static content. Based on the same LZ77 and Huffman foundations as DEFLATE, Brotli adds a static dictionary, larger matching windows, and improved context modeling to achieve an average 20% reduction in payload size compared to gzip.

Adopting a new compression standard requires weighing the tradeoffs and addressing ecosystem readiness. This guide outlines the practical engineering considerations for rolling out Brotli for static resources, from the initial decision to full serving and cache handling.

Weighing the Alternatives

Before committing resources to Brotli, it is worth evaluating the other ways to squeeze more out of existing compression setups.

  • Improving gzip: DEFLATE-based compression is capped by a 32kB window, a hard architectural ceiling. You can potentially unlock an additional 5-10% by pre-compressing at higher settings, using Zopfli-based tools, or even restructuring data, but the format will not surpass that limit.
  • Shared Dictionary Compression over HTTP (SDCH): This ambitious scheme can be very effective but remains unsupported by most browsers and server stacks. It also adds a complex step to maintain and version dictionaries, especially for localized or dynamic content.

A Practical Scope: Static Resources Only

For a first deployment, restricting Brotli to static assets provides significant optimization headroom and simplifies engineering. This limitation creates three key advantages:

  • Compression time is irrelevant: Assets can be processed offline with the most aggressive settings without impacting request latency.
  • Predictable file sizes: Knowing the exact source size allows for tuning the Brotli window to avoid wastage, and it enables the use of non-streaming compressors if necessary.
  • Full validation: You can run a complete compress-decompress cycle to ensure data integrity before serving, catching any library-specific bugs early in the pipeline.

Serving Brotli from the Server

Compressing data on the fly is a wasteful tax on CPU and memory for immutable resources. The nginx gzip_static module has long been the solution, but for Brotli, Piotr Sikora’s ngx_brotli module brings the same efficiency with a brotli_static directive. This allows nginx to directly serve pre-compressed .br files, bypassing runtime compression entirely. You can also combine this with the gunzip module to serve a client that does not support Brotli, or to save disk space by storing only the compressed versions.

Generating the .br Files

The Brotli source distribution includes C, Java, and Python bindings. You can use these in a build script or invoke the bro command-line tool directly to process files based on their MIME types.

When configuring this step, pay close attention to the window size. While larger windows yield better compression, they also demand more memory during both compression and decompression. Since mobile devices have tighter constraints, restricting the window parameter may be necessary. Furthermore, there is no point in setting the window size taller than the file itself. Window size is defined in bits, with the actual size computed as win_size = (1 << wbits) - 16. Brotli supports window sizes from 1KB up to 16MB under the current spec.

When Gzip Remains a Valid Target

However, if Brotli deployment faces significant engine-level hurdles, revisiting gzip is still worthwhile. Standard `zlib` compression at level 9 usually yields just under 1% gain over level 6, making that a poor investment for pre-compression. But third-party libraries can be much more effective:

  • Zopfli spends significantly more computational time but delivers 3-10% better ratios over standard zlib outputs.
  • Libdeflate achieves comparable ratios to Zopfli on a typical static asset set without the heavy CPU load, making it an excellent choice for offline pre-compression scripts.
  • 7-Zip in its deflate-compatible mode underperformed libdeflate in comparisons.

Handling the Middle: CDNs and Proxies

Deploying Brotli does not end with the origin server. Content Delivery Networks (CDNs) and other intermediaries can disrupt the negotiation process. Even when origins set the Vary: Accept-Encoding header, several risky behaviors can arise:

  • Header Normalization: Some CDNs normalize all client requests to Accept-Encoding: gzip to optimize cache storage, which prevents the origin from ever seeing a Brotli request.
  • Ignored Vary Header: A dangerous failure mode is a CDN caching a Content-Encoding: br response and serving it to clients that don't support it. This is why testing cache hit behavior regarding Accept-Encoding is crucial, and why the Vary header must be a core part of your cache key strategy.

Normalizing the Accept-Encoding header at the edge improves the cache hit rate, but reading its raw values reveals a long, unpredictable tail. A robust solution normalizes the header (e.g., to gzip, br) for upstream requests and keys the cache on that normalized value rather than the potentially enormous variety of client headers.

Proxies between the client and CDN also present a risky middlebox problem, as naive (non-TLS) proxies may misinterpret unknown content-encoding headers. Brotli mitigates this risk in the standards by being advertised only over HTTPS, protecting the negotiation from inspection. Even so, corporate TLS-intercepting proxies and antivirus scanners may fail to handle it, but these instances are rare and typically fixed by vendors quickly.

Client-Side Support Reality

Brotli's most robust aspect is its client-side penetration. Supported by Chrome for years, it’s backed by the open standard RFC 7932.

  • Firefox: Has supported it since version 45 (including its Extended Support Release).
  • Microsoft Edge: Initial support will arrive with version 15.
  • Safari: As of the Technological Preview, it still does not advertise Brotli compatibility.

One unforeseen bug specifically affected old Firefox 45 ESR (e.g., 45.0.*) versions, which advertised support but could not decode files where the compression window was smaller than the original file’s size. If you must serve these clients, you have two options: increase the installed window size for all files or use the brotli_disable directive in nginx to blacklist those specific user agents—essentially a Brotli release valve that behaves like gzip_disable for problematic browsers.

What the Numbers Show

Enabling Brotli reduced the average payload size fetched from the CDN by 20%. To read the charts, assets were grouped by type and by logarithmic size buckets from 32 bytes to 1MB, then compared against gzip -9 for both libdeflate and Brotli. Error bars denote the 95% confidence interval, with boxplots (IQR, median, min, max) provided for the Brotli dataset.

JavaScript and CSS

For uncompressed JavaScript, Brotli delivers 20–25% gains over zlib on both very small files (thanks to its static dictionary) and large ones (where its larger window helps). Medium-sized files see 13–17% improvements. libdeflate adds a respectable 4–5% on top of gzip.

Compression improvements over `gzip -9` for Javascript files broken down by original file size

Results hold for minified JavaScript, except at the largest file sizes where sample counts are too low for significance. libdeflate settles at a stable ~3% gain there.

Compression improvements over `gzip -9` for minified Javascript files broken down by original file size

CSS compresses even better under Brotli, with 20–30% improvements, likely due to the format’s inherent redundancy. libdeflate hovers around 4%.

Compression improvements over `gzip -9` for CSS files broken down by original file size

Language Packs

The largest Brotli wins show up on Cyrillic and Asian language packs, reaching 31%, most likely because their larger size benefits from Brotli’s bigger compression window. Other languages see about 25%, while libdeflate rates near 5%.

Compression improvements over `gzip -9` for different languages

The Missing Performance Win

The negative results here matter more than the positive ones: rolling out Brotli yielded only minor gains on the 90th-percentile Time-To-Interactive (TTI) metric. Several factors explain this:

  • Client-side caching works. Static files live in content-addressable storage, and proper handling of If-None-Match and If-Modified-Since keeps cache-hit disruptions minimal during deployments.
  • CDNs are fast. With a global network of over 100 Points of Presence, download speed is rarely the bottleneck.
  • Most visitors use stable connections. Mobile ISP users tend to use native apps, which don’t hit the CDN for static assets.
  • Not every browser supports Brotli — Safari being the notable holdout.
  • Modern pages are JavaScript-heavy. Parse, compile, and execution time often dominate network transfer, making CPU the constraint rather than bandwidth.

A 20% reduction in static bytes won’t translate into a 20% TTI win. The improvement still matters for infrequent visitors on lossy wireless links or metered mobile connections. Pre-compression also eliminated CPU cost from the web tier serving static content, cutting overall CPU usage by about 15%.

Next Steps

Compressing dynamic responses with Brotli is the logical follow-up, but compression speed becomes critical there. The Squash benchmark gives a rough view of the tradeoff:

enwiki8 compression ratio vs compression speed (Here, we’d like to be as close to the top-right corner as possible — both fast compression speeds and high compression ratios.)

Dynamic compression requires optimizing for compression_time + transfer_time + decompression_time while keeping internal proxy buffering low. The zlib path in nginx’s gzip module is a cautionary example where enabling gzip without disabling proxy buffering causes excessive buffering. Time-To-First-Byte and Time-To-Last-Byte will be monitored closely during rollout. Preliminary data suggests Brotli quality 5 offers the right speed-to-ratio balance for dynamic data.

Images account for only about 10% of downloaded bytes on average, though landing pages can reach 80%. Brotli won’t help there, but other options exist for build pipelines:

  • For JPEG, vary quality, try different progressive scans, or switch encoders like mozjpeg or guetzli. All require access to original, uncompressed images since recompressing JPEG leads to poor results.
  • PNG is deflate-based, so it responds to the same zlib tricks. Zopfli handles lossless optimization via zopflipng; other tools like advancecomp also work.
  • A lossy 24-bit RGBA-to-8-bit palette conversion with pngquant is another route.
  • Encoding images in multiple formats and serving webp where the CDN supports Accept header-based caching works for capable browsers.

Sprite files are already optimized with pngquant, shrinking them to ~25% of original size and cutting roughly 300kB per page. Running ZopfliPNG/advpng on top yields another 5%.

One useful quirk: because gzip’s back-reference distance is limited, grouping visually similar sprites together improves compression. The same principle applies to any deflate-compressed data — language packs are sorted this way for better ratios.

The entire effort — modifying the static asset pipeline, adding pre-compressed file support to web servers, adjusting CDN cache behavior, testing, and deploying — took about two weeks. That one-time work now yields a permanent 20% reduction in static asset footprint.

Appendix: Inside the Brotli Format and Tooling

For readers who want deeper detail on the Brotli algorithm itself, the WebFonts Working Group has published two useful primers: a comparison of Brotli to gzip and a specification outline. The canonical reference remains RFC 7932; despite its length, roughly two thirds of the document is a hex dump of Brotli's static dictionary.

Earlier work on using Brotli for backend compression led to modifications for significantly faster compression, published as the "q9.5" branch of the official Brotli repository. For static asset delivery described here, however, maximum-quality settings are used, since offline compression makes slower encode times acceptable in exchange for the best ratios.

Inspecting Compressed Output

Brotli's research directory contains a number of utilities aimed at dissecting the file format and visualizing backreferences. In particular, brotlidump.py is a standalone parser for Brotli-compressed data, useful for inspecting resulting .br files—for example, to confirm the window size or examine context maps.

Custom Dictionary Support

Compressing internally, especially when handling many small files with similar structure—such as JSON or protobuf payloads from one API—opens the door to custom dictionaries. This mirrors the SDCH approach: it avoids relying on Brotli's built-in static dictionary to match the dataset and eliminates the overhead of rebuilding a near-identical dynamic dictionary per tiny file. Custom dictionary support is exposed via Brotli's Python bindings, and Vlad Krasnov's dictator tool provides a practical way to construct dictionaries from a representative file sample.