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