Read more

How to make your application assets cachable in Rails

Henning Koch
September 13, 2012Software engineer at makandra GmbH

Note: Modern Rails has two build pipelines, the asset pipeline (or "Sprockets") and Webpacker. The principles below apply for both, but the examples shown are for Sprockets.


Illustration online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
Read more Show archive.org snapshot

Every page in your application uses many assets, such as images, javascripts and stylesheets. Without your intervention, the browser will request these assets again and again on every request. There is no magic in Rails that gives you automatic caching for assets. In fact, if you haven't been paying attention to this, your application is probably broken in that respect and users will re-request every single image etc. on every page load.

While ETags Show archive.org snapshot will usually prevent the image data from being transmitted multiple times, the browser will open one HTTP request for every asset and block other assets from being loaded until the server responds with 304 not modified. This will slow down your page loading considerably, and cause unnecessary load on your servers.

This card describes how you can setup your application in a way that browsers can properly cache your assets.

The caching conundrum

A caching solution must satisfy two requirements:

  • Allow browsers to cache assets by delivering files with an Expires header in the far future
  • Make sure that when the asset changes, the browser does not see the old, cached version and does make a new request ("stale cache")

These requirements are sort of conflicting: If you just told the browser that it may cache images/foo.png for 10 years, and you make a change to foo.png, the browser will only notice the change after 10 years.

How Rails solves the caching conundrum

A solution to the conundrum is to attach a timestamp or content hash to your asset's filenames, e.g. foo.png becomes foo-2179b43e243cf343.png. This way, when the asset changes, its URL changes. You can now safely tell browsers to cache asset URLs for many years, because when the asset changes, you will no longer use the old URL, and the browser will eventually discard the orphaned cache entry. Your Apache/Nginx/whatever should be setup in a way Show archive.org snapshot that it sets a faraway cache expiry date whenever it delivers a hashed asset.

However, if you want Rails to generate these magic hashed asset paths for you, the asset path must be produced by a Rails helper that is aware of this technique. All Rails helpers that link to assets, such as stylesheet_tag or image_tag, will output hashed asset paths. E.g. image_tag('foo.png') will output:

<img src="/assets/foo-2179b43e243cf343.png" />

There is also image_path or asset_path if you need the raw asset hashed paths without an HTML tag.

Whenever you don't pipe an asset path through a Rails helper, you don't get hashes. E.g. %img{:src => 'foo.png'} in Haml will produce a hashless <img>-tag and the image can't be cached properly:

<img src="/assets/foo.png" />

Note that you also refer to other images in your stylesheets (background-image: url(/path/to/asset)) and sometimes in your Javascripts. You need to route all these paths through Rails or you won't get hashed paths. Techniques to do this can be found below.

Rails 3

When an asset path goes through a Rails 3 helper, the helper attaches an MD5 hash of the file content to the asset. E.g. /app/assets/foo.png becomes:

/assets/foo-2179b43e243cf343.png

These hashes will only be added when assets.digests is enabled for your environment (it's enabled by default in production). Rails will actually copy your asset file from /app/assets to public/assets when the assets:precompile Rake task is run, usually during deployment.

Here are some good and bad examples. Note how every example will be cached if your server is configured to set Expire headers for everyhing in /assets, but only with hashes will the browser notice if an asset changes on the server:

Expression Output Will be cached? Browser notices if asset changes?
%img{:src => '/assets/foo.png') <img src="/assets/foo.png" /> yes no
%img{:src => image_path('foo.png')) <img src="/assets/foo-2179b43e243cf343.png" /> yes yes
=image_tag('foo.png') <img src="/assets/foo-2179b43e243cf343.png yes yes
background-image: url(/assets/foo.png) background-image: url(/assets/foo.png); yes no
background-image: image-url('/assets/foo.png') background-image: url(/assets/foo-2179b43e243cf343.png); yes yes

Rails 2

When an asset path goes through a Rails 2 helper, the helper attaches a timestamp to the asset. E.g. /images/foo.png becomes:

/images/foo.png?12304678

Timestamps will only be added when caching is enabled for your environment (it's enabled by default in production).

Note: The web server will not set Expire headers for all assets, only for those that are requested with a timestamp (like /images/foo.png?12304678). Therefore not all assets are cached by default.

The helpers are very similiar to Rails 3, except that your files don't live in /assets and you need a plugin to get timestamps in CSS background images:

Expression Output Will be cached? Browser notices if asset changes?
%img{:src => '/images/foo.png') <img src="/images/foo.png" /> no yes
%img{:src => image_path('foo.png')) <img src="/images/foo.png?12304678" /> yes yes
=image_tag('foo.png') <img src="/images/foo.png?12304678" /> yes yes
background-image: url(/images/foo.png) background-image: url(/images/foo.png); no yes
background-image: url(/images/foo.png) plus css_asset_tagger plugin Show archive.org snapshot background-image: url(/images/foo.png?12304678); yes yes
Henning Koch
September 13, 2012Software engineer at makandra GmbH
Posted by Henning Koch to makandra dev (2012-09-13 16:43)