Honey, I shrunk the internet! - Content Compression via Rack::Deflater

Dan Collis-Puro

Speed is key. The snappier the site, the more visitors like you. Speed is so important that Google uses it in site rankings and as a major component of its PageSpeed Tools.

Rack::Deflater middleware compresses responses at runtime using deflate or trusty ol’ gzip. Inserted correctly into your Rack app, it can drastically reduce the size of your HTML / JSON controller responses (numbers below!). On a Heroku Rails 3 deployment, it can be configured to compress assets delivered via your dynos.

There are other (possibly better) places to handle content compression, for example:

  • A frontend proxy / load balancer,
  • Your CDN,
  • By pre-compressing content and serving that from your web server.

We’re going to talk about the simplest thing that’ll work for most Heroku hosted Rails apps.

Rails 3 and 4

Add it to config/application.rb thusly:

module YourApp
  class Application < Rails::Application
    config.middleware.use Rack::Deflater
  end
end

And your HTML, JSON and other Rails-generated responses will be compressed.

Rails 3 and Runtime Asset Compression

If you’re running Rails 3 (which will serve static assets for you), you can use Rack::Deflater for runtime asset compression as well. Configure it thusly:

module YourApp
  class Application < Rails::Application
    config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
  end
end

Inserting Rack::Deflater before ActionDispatch::Static means you’ll get runtime compression of assets served from Heroku in addition to the HTML, JSON, XML and other content your app returns.

Rails 4 assumes you’re serving assets from a CDN or via your webserver (and not your Rails processes) so ActionDispatch::Static middleware isn’t enabled by default. If you try to insert Rack::Deflater before it, you’ll get errors.

spec'ing

Controller specs skip Rack middleware. You need to assert that content is compressed / not compressed in a feature spec as they exercise the full Rails stack. We’re using Capybara’s RSpec awesomeness for our integration specs. Example:

# spec/integration/compression_spec.rb
require 'spec_helper'

feature 'Compression' do
  scenario "a visitor has a browser that supports compression" do
    ['deflate','gzip', 'deflate,gzip','gzip,deflate'].each do|compression_method|
      get root_path, {}, {'HTTP_ACCEPT_ENCODING' => compression_method }
      response.headers['Content-Encoding'].should be
    end
  end

  scenario "a visitor's browser does not support compression" do
    get root_path
    response.headers['Content-Encoding'].should_not be
  end
end

Compression Overhead - Dynamic Content

There is overhead to compressing content - let’s see how significant it is. These tests were run against a fairly typical Rails 3.2 app.

We’ll run Siege against a dynamic content page running under Thin for 30 seconds, simulating 10 concurrent users. I’m picking typical results: I ran Siege numerous times for each scenario.

siege -t30s -c 10 'http://127.0.0.1:3000/contact'

| |Before Rack::Deflater|After Rack::Deflater| |Transactions|271 hits|265 hits| |Data transferred|1.24 MB|0.45 MB| |Transaction rate|9.19 trans/sec|8.93 trans/sec|

So we were a little slower, but the content is around 1/3rd the size.

Compression Overhead - Static Content

Let’s try against static asset content.

siege -t10s -c 10 'http://localhost:3000/assets/application.css'

| |Before Rack::Deflater|After Rack::Deflater| |Transactions|22601 hits|4052 hits| |Data transferred|658.26 MB|21.44 MB| |Transaction rate|2374.05 trans/sec|443.33 trans/sec|

So when we benchmark the raw speed of compressing static assets, it’s around 5 times slower. The benefit, though, is that application.css is around 6 times smaller - 6.2KB instead of 30.1KB.

Plus we’re still serving 443 requests per second. That is far beyond the demands that’d be put on any one dyno, and at traffic levels like this you’re probably already using a CDN.

Real world impact

Once you’ve enabled Rack::Deflater middleware, you should see compression statistics in the Chrome web inspector for (at least) your Rails-generated content. For example:

Compression Results

Assets are also being compressed in this Rails 3.2 app via Rack::Deflater, hence the compression ratios for application.css and application.js.

| |Before Rack::Deflater|After Rack::Deflater|Compression Rate| |Google PageSpeed analysis|79 of 100|93 of 100|N/A| |application.css|30.1KB|6.2KB|79%| |application.js|117.0KB|40.8KB|65%| |page html|4.3KB|2.6KB|40%| |Total size|151.4KB|49.6KB|67%|

So with minimal effort we were able to decrease our page sizes significantly (1/3rd of their original size!) and bump up our PageSpeed analysis 14 points.