Skip to content

Rails Response and Assets Compression on Heroku #7

Open
@winston

Description

@winston

Update 10 Jan 2016: The earlier benchmarks were ran against a Rails 4.2.4 (with Sprockets v3.4.0) app where gzip compression was missing.

@schneems has since reintroduced gzip compression in v3.5.0 (see commit rails/sprockets@7faa6ed), and so I ran the baseline again with Rails 4.2.5 and more importantly with Sprockets v3.5.2. Results:

Baseline Updated with Sprockets v3.5.2

Demo at http://rails-heroku-baseline-updated.herokuapp.com

Let's take another look at our baseline - how a basic Rails 4.2.5 app performs out of the box.

screen shot 2016-01-09 at 11 30 17 pm

As compared to the previous baseline (using Rails 4.2.4 and Sprockets 3.4.0), you can see that in this updated baseline, the application.css and application.js are both gzipped.

screen shot 2016-01-09 at 11 57 28 pm

In total, 571KB was transferred and it took about 3.66s for the page to load.

screen shot 2016-01-10 at 12 00 12 am

When we run Page Speed Insight on this app, we get a score of 64/100 and Enable compression is top of the "Should Fix" list, but it's only for the web request (and not the assets).

This means that we only need to fix the problem of gzipping our web response. Read on!


@heroku is awesome, in that you can deploy a Ruby app in less than 5 minutes up into the internet. However, in exchange for that convenience, we are not able to configure web server settings easily (unless you launch your own Nginx buildpack, for example).

On the other hand, speed really is king and every website aims to be speedier for many, many reasons. Two being for a better user experience and for a better site ranking (according to Google).

One of the most commonly suggested advice in speeding up a website is to enable compression and serve gzipped responses and gzipped assets (JS and CSS) which can be easily configured on the server (Nginx) level.

However, we can't really do that on vanilla Heroku and so we have to explore alternatives.

There are a number of ways we can have content compression on vanilla Heroku, and this post is for exploring those different ways.

tl;dr You can use the heroku-deflater gem.


For the purpose of exploring different ways to achieve content compression on vanilla Heroku, I created a simple Rails 4.2 app with the following gems:

ruby '2.2.3'

gem 'puma'
gem 'pg'

gem 'slim-rails'

gem 'bootstrap-sass'
gem 'font-awesome-sass'

group :staging, :production do
  gem 'rails_12factor'
end

Next, I generated a scaffold for blog_post with title and content as attributes and populated the database 1500 blog posts using seeds.rb.

The source code is available here: https://github.com/winston/rails-heroku-compression

Goals

Our goal is to find out which method is better for achieving compression on:

  • web response
  • application.css
  • application.js

Essentially, these responses should be gzipped and be small in size.

Baseline

Demo at http://rails-heroku-baseline.herokuapp.com

Let's first look at how a basic Rails app performs out of the box.

baseline

The size of the web response is about 431KB, application.css 148KB and application.js 156KB.

In the Content-Encoding column, you can see that all three are not encoded (gzipped) in anyway.

baseline-total

In total, 799KB was transferred and it took about 3.25s for the page to load.

baseline-psi

When we run Page Speed Insight on this app, we get a score of 56/100 and Enable compression is top of the "Should Fix" list.

Rack Deflater

Demo at http://rails-heroku-rack-deflater.herokuapp.com

In this branch, we added a middleware that would perform runtime compression on the web response. However, it doesn't compress CSS or JavaScript.

# Added to config/application.rb

module RailsHerokuCompression
  class Application < Rails::Application
    # ...

    config.middleware.use Rack::Deflater
  end
end

Let's look at how it performs.

rack-deflater

The size of the web response is now 24.5KB and "Content-Encoding" appears as gzip, while application.css and application.js remains unchanged.

That's a saving of about 94% in size!

rack-deflater-total

In total, 392KB was transferred and it took about 3.52s for the page to load.

Even though the total size was reduced by about 50%, however on the average with Rack::Deflater, this branch seemed to have taken just a bit more time than the baseline to load. That's because compression was done during runtime, and that could have resulted in a slight slowdown, as shared by @thoughtbot too.

rack-deflater-psi

When we run Page Speed Insight on this app, we get a score of 70/100 which is an increase of 14 points over baseline.

Assets Gzip

Demo at http://rails-heroku-assets-gzip.herokuapp.com

In this branch, we are only concerned about compressing our assets.

This is important because compression has been removed from Sprockets 3 (affects Rails 4), so we need to do this "manually" for now, until maybe the next version of Sprockets.

Of course, other than doing this on the server, you can explore using a CDN like fastly that could do the compression of assets but we'll leave that to a separate discussion.

# Added to lib/assets.rake
# Source: https://github.com/mattbrictson/rails-template/blob/master/lib/tasks/assets.rake

namespace :assets do
  desc "Create .gz versions of assets"
  task :gzip => :environment do
    zip_types = /\.(?:css|html|js|otf|svg|txt|xml)$/

    public_assets = File.join(
      Rails.root,
      "public",
      Rails.application.config.assets.prefix)

    Dir["#{public_assets}/**/*"].each do |f|
      next unless f =~ zip_types

      mtime = File.mtime(f)
      gz_file = "#{f}.gz"
      next if File.exist?(gz_file) && File.mtime(gz_file) >= mtime

      File.open(gz_file, "wb") do |dest|
        gz = Zlib::GzipWriter.new(dest, Zlib::BEST_COMPRESSION)
        gz.mtime = mtime.to_i
        IO.copy_stream(open(f), gz)
        gz.close
      end

      File.utime(mtime, mtime, gz_file)
    end
  end

  # Hook into existing assets:precompile task
  Rake::Task["assets:precompile"].enhance do
    Rake::Task["assets:gzip"].invoke
  end
end

Let's look at how it performs.

assets-gzip

The web response in this case remains un-gzipped at 431KB.

The size of application.css is now 26.4KB (down from 148KB) and "Content-Encoding" is gzip while the size of application.js is now 48.5KB (down from 156KB) and "Content-Encoding" is gzip too.

assets-gzip-total

In total, 569KB was transferred and it took about 3.22s for the page to load.

assets-gzip-psi

When we run Page Speed Insight on this app, we get a score of 59/100 largely because the web response wasn't compressed.

Heroku Deflater

Demo at http://rails-heroku-heroku-deflater.herokuapp.com

In this branch, we will be using the heroku-deflater gem.

# Added to Gemfile

group: stagimg, :production do
  gem 'heroku-deflater'
end

Let's look at how it performs.

heroku-deflater

The web response is now 24.5KB (down from 431 KB), identical to when Rack::Deflater was used, while application.css is now 26.7KB and application.js is now 49.5KB.

All of them have "Content-Encoding" as gzip.

heroku-deflater-total

In total, 164KB was transferred which translates to a savings of 79% from the baseline measurement, and it took about 2.64s for the page to load.

heroku-deflater-psi

When we run Page Speed Insight on this app, we get a score of 87/100 and it no longer complains about "Compression".

Optimized

Demo at http://rails-heroku-optimized.herokuapp.com

At this point, heroku-deflater has given us the best results so far with everything compressed.

Looking beneath the hood, heroku-deflater is actually simply using Rack::Deflater for "all" requests.
But if a gzipped version of the file already exists, then it would serve up that file immediately and not compressed it every single time.

With this in mind, I decided to try and combine both "Assets Gzip" and "Heroku Deflater" into this branch.

Let's look at how it performs.

optimized

The web response is still compressed at 24.5KB while application.css and application.js are both at the better compression of 26.5KB and 48.5KB (due to "Assets Gzip").

optimized-total

In total, there's also a slight reduction to 163KB sent and it took 2.91s to load the page.

optimized-psi

When we run Page Speed Insight on this app, we get an even more impressive score of 89/100!

Conclusion

App Web Response application.css application.js Total Size Total Time
baseline 431KB 148KB 156KB 799KB 3.25s
rack-deflater 24.5KB 148KB 156KB 392KB 3.52s
assets-gzip 431KB 26.4KB 48.5KB 569KB 3.22s
heroku-deflater 24.5KB 26.7KB 49.5KB 164KB 2.64s
optimized 24.5KB 26.5KB 48.5KB 163KB 2.91s

Rails doesn't do any compression out of the box, and if you are deploying on Heroku, a quick fix would be to use heroku-deflater.

If you are deploying your apps on non-Heroku boxes, then I am sure you will be able to tweak Nginx's server configurations to make compression work even more easily.

Besides doing such compression, it's also a good practice to put your apps behind CDNs too, as that would make your app even speedier.

In summary, don't forget to shrink your app before you deploy!

Notes:


Thank you for reading.

@winston ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions