GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

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

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  
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.