Rails · HTTP compression · Propshaft

Brotli, gzip, and Propshaft in Rails

Brotli usually gives smaller text assets than gzip, but where compression runs matters more than the algorithm name. In a Rails app using Propshaft, static assets should ideally be compressed before or at the edge, not inside the Rails request path.

Brotli vs gzip

Brotli and gzip are both lossless compression formats commonly used for HTTP responses. They are most useful for text-like payloads: HTML, CSS, JavaScript, JSON, SVG, XML, and similar formats. They are usually not useful for media formats that are already compressed, such as JPEG, PNG, WebP, MP4, ZIP, or PDF.

Gzip is older, widely supported, and fast. It is based on the DEFLATE family of compression techniques. Brotli is newer and is designed to produce denser output for web content. Google's Brotli implementation describes it as combining an LZ77 variant, Huffman coding, and second-order context modeling. In practice, Brotli often produces smaller text assets than gzip, especially when run at higher quality levels during a build step.

Question Brotli Gzip
Best use Static text assets, especially precompressed assets Dynamic responses and broad fallback support
Compression ratio Usually better for text at comparable settings Good, but commonly larger than Brotli
Compression cost Can be expensive at high quality levels Usually cheaper and faster
Decompression Fast enough for normal web delivery Fast and universally mature

The practical rule is simple: use Brotli for assets that can be compressed once and served many times. Keep gzip enabled as a fallback.

How HTTP negotiates compression

Browsers advertise which encodings they can decode with the Accept-Encoding request header. The server chooses one and declares the result with Content-Encoding.

Accept-Encoding: br, gzip, deflate
Content-Encoding: br

This matters because the original URL usually stays the same. A request for /assets/application-abc123.css may return Brotli, gzip, or an uncompressed response depending on browser support, server configuration, and cache state.

Any cache in front of Rails must respect that negotiation. In normal HTTP caching, compressed variants should be keyed so that a Brotli-capable browser does not receive a gzip-only response by mistake, and vice versa. Servers and CDNs normally handle this through encoding-aware caching and the Vary: Accept-Encoding response header.

What rack-brotli does

rack-brotli is Rack middleware that compresses Rack responses using Brotli. Its README shows it being inserted into the Rack stack with use Rack::Brotli, or in Rails with config.middleware.use Rack::Brotli.

config.middleware.use Rack::Deflater
config.middleware.use Rack::Brotli

The useful distinction is where the compression work happens. Rack middleware runs inside the Ruby application process. If it handles a compressible response, the application process is involved in compression. That may be fine for HTML or JSON responses at moderate quality settings, but it is usually the wrong place to repeatedly compress long-lived static assets.

Do not assume that adding Brotli middleware means static assets are precompressed or served without runtime work. Middleware can compress responses, but precompressed assets are separate files such as:

application-abc123.js
application-abc123.js.br
application-abc123.js.gz

When those files exist and the web server is configured for static Brotli or static gzip, the web server can serve the compressed file directly. Rails does not need to generate the compressed bytes during the request.

What changes with Propshaft

Propshaft is the modern Rails asset pipeline. The Rails Guides describe it as focusing on essential asset management tasks such as organizing, fingerprinting, and serving browser-ready assets. More complex processing, including bundling and minification, is left to specialized tools.

That design is relevant to compression. With Propshaft, do not treat assets:precompile as a guarantee that .br or .gz files will be produced for every JavaScript or CSS file. In many setups, Propshaft fingerprints and copies assets, while compression is handled by another step, the web server, or a CDN.

So the answer to the operational question is: in a Propshaft app, rack-brotli can compress asset responses at runtime if those responses pass through Rails and are not already served as precompressed files by another layer. Whether that happens on every request depends on the full stack: Rails static serving, middleware order, cache headers, reverse proxy behavior, and CDN caching.

For static assets, prefer this order:

  1. Build or copy browser-ready assets with Propshaft.
  2. Generate .br and .gz files during deployment or build, or let a CDN generate and cache compressed variants.
  3. Serve assets from a web server or CDN with long-lived cache headers.
  4. Keep Rails out of the asset hot path as much as possible.

A typical static-compression deployment produces files like this:

public/assets/application-abc123.css
public/assets/application-abc123.css.br
public/assets/application-abc123.css.gz
public/assets/application-def456.js
public/assets/application-def456.js.br
public/assets/application-def456.js.gz

The web server can then serve the right variant based on Accept-Encoding. A CDN can do the same at the edge. Either approach avoids spending Ruby CPU on the same immutable asset again and again.

When runtime compression is acceptable

Runtime compression is not automatically bad. It is often reasonable for dynamic HTML, JSON, or API responses where the exact body is not known at build time. The tradeoff is CPU for bandwidth.

For dynamic responses, gzip at a moderate level is often a practical default. Brotli can also be used, but high Brotli quality levels are usually better reserved for offline compression. For live responses, lower Brotli quality settings are safer if latency and CPU are sensitive.

For static assets, the calculus is different. A fingerprinted asset can be compressed once and served for a long time. Recompressing it in Rack is avoidable work.

Practical checklist

  • Check whether public/assets contains .br and .gz files after deployment.
  • Check response headers for real asset requests, not only Rails controller responses.
  • Look for Content-Encoding: br or Content-Encoding: gzip.
  • Look for Cache-Control and Vary: Accept-Encoding.
  • Confirm whether assets are served by Rails, Nginx, Caddy, Apache, a platform router, or a CDN.
  • Use Rack compression mainly for dynamic responses, unless your deployment constraints require otherwise.
  • Keep gzip fallback enabled even if Brotli is available.

A simple check with curl:

curl -I -H 'Accept-Encoding: br, gzip' https://example.com/assets/application-abc123.css

Expected signs for a good static asset setup include a long cache lifetime, an appropriate Content-Encoding, and no application-level work for repeat requests once the CDN or web server cache is warm.

Sources

  1. Google Brotli README, description of Brotli as a lossless compression algorithm using an LZ77 variant, Huffman coding, and context modeling.
  2. RFC 7932, Brotli compressed data format.
  3. MDN: Accept-Encoding, HTTP content negotiation for compression encodings.
  4. MDN: Content-Encoding, response header used to identify applied encodings.
  5. rack-brotli README, Rack middleware usage and Rails insertion example.
  6. Ruby on Rails Guides: The Asset Pipeline, Propshaft responsibilities and modern asset-pipeline behavior.
  7. rails/propshaft issue 86, discussion around gzip and Brotli compressed asset generation being outside Propshaft's core scope.