Conditionally Enable GZIP on Heroku with Rack::Deflater: Reduce Response Size Significantly

… and check why 5600+ Rails engineers read also this

Conditionally Enable GZIP on Heroku with Rack::Deflater: Reduce Response Size Significantly

If you came here after searching for something like “rack deflater path condition” or “rack deflater if option,” here is your answer:

config.middleware.use(
  Rack::Deflater,
  :if => lambda do |env, _, _, _| 
    env["PATH_INFO"] == "/your/endpoint/path/here" 
  end
)

Just insert this line in your application.rb, and you’re set. We could wrap up this post here, but… But we all know that we should not take a piece of random information from the Internet for granted. No matter what the most advanced, AI-powered search engines in the world may suggest, adding non-toxic glue to pizza sauce is never a good choice.

Backstory

Let’s start by explaining how we ended up with such a config. Recently, we have been working on performance improvements of the project’s most complex (from the UI perspective) page. Well, basically, those are just… two tables and a chart. But those tables are big, like very big, as they usually contain a few thousand cells. It’s a lot of HTML when “hotwired"😉😉. You could say now that with JSON, it would be less data, but well, I love how Hotwire works, and I totally agree with this statement from their handbook:

Yes, the HTML payload might be a tad larger than a comparable JSON, but with gzip, the difference is usually negligible, and you save all the client-side effort it takes to fetch JSON and turn it into HTML.”

And that was actually what… did not happen in our case 😉. At some point, we noticed that downloading the response took longer than waiting for the server on my Internet connection.

You can spot the huge response size, the missing Content-Encoding header, and the download time here. The client was sending the Accept-Encoding: gzip, deflate, br, zstd header, so we just double-checked using cURL. Use the --write-out option with the size_download variable to see the download size.

The downloaded content size was the same for both requests (with and without the Accept-Encoding header).

This means the server does not support compression by default. As the project is hosted on a standard Heroku setup, there is nothing like a reverse proxy there (with configured compression). In such a case, Heroku tells us to compress on the application side.

A small disclaimer here is that for this project, the responses’ compression is not needed in most cases as we do exact updates of page elements. Compression and then decompression of such small pieces of content would make no sense. You may want to see how we usually work with Hotwire in this YouTube episode: Make your tables alive with turbo streams. Redirect vs Turbo Streaming. Which one to choose?.

Moreover, we even disabled “gzipping” for assets (config.assets.gzip = false) so the CDN could do it better. There is already a post about it here: Don’t waste your time on assets compilation on Heroku.

Reasoning behind going with Rack::Deflater

For this specific case and given timebox conditions, we considered enabling a Rack::Deflater conditionally a good option. With this solution we reduced the response size from ~5MB to ~100KB 😉. It was very satisfying result so we decided to give it a try.

We will probably stay with it as long as we don’t need compression for many more endpoints (which is doubtful, given our way of working with Turbo Frames and Turbo Streams).

Food for thought

In the long term, we could gather some metrics about response size and find some sweet spot for which responses (above which size, for example) we should compress. Then we could move the compression part to the revere proxy (like nginx) using, for example, the heroku-buildpack-nginx. Thanks to that, the app itself would not need to spend resources on compression, and that would be done by a specialized tool that supports not only GZIP but also Brotli, for example.

You might also like