Generating Gzipped Assets for Nginx in Rails 4.2

Nginx has the ability to serve static .gz files instead of compressing assets on the fly, which is great for performance. Unfortunately, Rails recently dropped its support of this feature. Here’s how to restore it.

Background

In older versions of Rails (specifically, before Sprockets 3 was introduced), you may have noticed that assets:precompile generated .gz versions of all the assets. A properly-configured Nginx web server can serve these gzipped versions instead of the uncompressed originals.

This is a great feature, because:

  • Browsers get compressed responses, which means faster transfer times and happier users
  • Since the gzipped version is pre-generated, Nginx doesn’t need to perform compression on the fly
  • Furthermore, we can use gzip’s maximum compression setting, since we don’t have to pay the CPU penalty on each web request

Unfortunately, this gzip feature was removed from the asset pipeline in Sprockets 3, and this generated a lot of discussion on GitHub. The most insightful comments can be found on the commit itself, and there has been some good feedback in a subsequent feature request.

As it turns out, the pre-generated gzip feature works great with Nginx, but causes subtle bugs with the Apache web server, which is also popular. Rather than enable by default a feature that breaks a significant portion of web servers, the Sprockets team decided not support the feature at all.

The good news is that the Rails team has stepped in and is planning to bring the feature back via an opt-in at some point. But what can we do in the meantime?

Implementing a workaround

If you use Nginx, chances are you want this feature back. Concerned developers have put forth various workarounds.

The solution I use is to extend the assets:precompile rake task to perform the gzipping. Here’s how I’ve implemented the rake task (which, by the way, is baked into any Rails app generated with my Rails template):

# Place this code in 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

Note that:

  • The .gz is only created if the original is newer
  • It’s important to set the right modification time for the generated .gz
  • Only “compressable” file extensions are considered (e.g. not images)

Configuring Nginx

Finally, it is worth a reminder that all of this gzip work is effective only if Nginx is properly configured. Specifically, the gzip_static directive is that magic that instructs Nginx to look for the matching .gz files when an asset is requested.

Here’s the proper config, which you can also find mentioned in the old 4.1.x version of the asset pipeline documentation:

location ~ ^/(assets)/ {
  gzip_static on;
}
You just read

Nginx has the ability to serve static .gz files instead of compressing assets on the fly, which is great for performance. Unfortunately, Rails recently dropped its support of this feature. Here’s how to restore it.

July 2015

Share this post?  Copy link

About the author

Hi! I’m a Ruby and CSS enthusiast, regular open source contributor, software engineer, and occasional blogger writing from the San Francisco Bay Area. Thanks for stopping by! —Matt

GitHub Email LinkedIn