The Asset Pipeline
The asset pipeline gives the Rails developer a way to deliver CSS, JavaScript and image files to the browser optimally. Files get fingerprinted (a digest is added to the filename) so that browsers and any proxy in between can cache them aggressively — the digest changes whenever the file changes, so a fresh deploy always invalidates the cache automatically.
Rails 8 ships two pieces to handle this:
-
Propshaft, the asset pipeline itself.
-
importmap-rails, which serves and pins JavaScript modules without a Node.js build step.
They are both configured out of the box in a fresh
rails new myapp.
Rails 5/6/7 used Sprockets for the asset pipeline and, for
JavaScript, Webpacker (Rails 5/6) or jsbundling-rails (Rails
7). Propshaft and importmap-rails are the new defaults
since Rails 7. Sprockets still works if you need it — add
gem "sprockets-rails" and Rails will use it instead of
Propshaft.
|
As an example we use once more our web shop with a product scaffold:
$ rails new webshop
[...]
$ cd webshop
$ bin/rails generate scaffold product name 'price:decimal{7,2}'
[...]
$ bin/rails db:migrate
[...]
In the directory app/assets you will find the following files:
app/assets/
├── images
└── stylesheets
└── application.css
And in app/javascript:
app/javascript/
├── application.js
└── controllers
├── application.js
├── hello_controller.js
└── index.js
Propshaft serves anything below app/assets/* and any other
path you add to Rails.application.config.assets.paths.
importmap-rails serves the files under app/javascript/ as ES
modules.
application.css
app/assets/stylesheets/application.css is the entry point for
your stylesheet bundle. In a fresh Rails 8 app it looks like
this:
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* [...]
*/
Propshaft does not concatenate or preprocess CSS; each .css
file under app/assets/stylesheets/ is served independently,
with its filename fingerprinted. Write CSS the way you’d write it
for any static site and let the browser cache each file on its
own.
If you want SASS/SCSS you can add the dartsass-rails gem (for
SCSS) or tailwindcss-rails (for Tailwind). Both compile your
source to plain CSS during bin/rails assets:precompile.
application.js
app/javascript/application.js is the entry point for your
JavaScript bundle:
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"
With importmap-rails you import a module name (like
"@hotwired/turbo-rails") and the import map tells the browser
which URL corresponds to that module. This is ES modules as the
browser natively supports them — no bundler, no Node step, no
build output to commit or deploy.
The import map itself lives in config/importmap.rb:
# Pin npm packages by running ./bin/importmap
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
You add JavaScript packages with the importmap binstub:
$ bin/importmap pin lodash
Pinning "lodash" to https://ga.jspm.io/npm:lodash@4.17.21/lodash.js
After that you can write import _ from "lodash" in any of your
files under app/javascript/.
Import maps are ideal when your JavaScript is small-to-
medium and you don’t need a heavy framework like React. If
you do need React/Vue/Svelte or a true bundler, add
gem "jsbundling-rails" and run rails new myapp --javascript=esbuild
(or bun/rollup/webpack). Propshaft keeps handling the
CSS and static assets in that setup too.
|
Images and Other Assets
Images go in app/assets/images/. They are fingerprinted just
like CSS. Reference them from ERB via asset_path or the
purpose-specific helpers:
<%= image_tag "rails.png" %>
<%= asset_path("rails.png") %>
<img src="<%= asset_path("rails.png") %>" alt="Rails">
In CSS use the url(…) helper:
.logo { background: url("rails.png"); }
Precompile In Production
When you deploy you run:
$ RAILS_ENV=production bin/rails assets:precompile
This copies every asset from app/assets/ and app/javascript/
into public/assets/, each file’s name suffixed with its SHA-256
digest, and writes a .manifest.json map that translates logical
names to fingerprinted names at runtime. Rails 8’s default
Dockerfile runs this command as one of the build steps, and so
does Kamal when it builds a deploy image.
Because every file in public/assets/ is static, you should let
your web server (Thruster, Nginx, a CDN) serve them directly, no
Ruby involvement.
Live Recompile In Development
In development Propshaft re-serves the latest version of a file
on every request. No cache bust, no server restart. Just edit
app/assets/stylesheets/application.css or
app/javascript/application.js and reload.
For tailwindcss-rails or dartsass-rails you usually start the
watcher alongside the Rails server by using bin/dev:
$ bin/dev
bin/dev reads Procfile.dev (generated by the CSS or JS
framework when you add it) and runs all the processes you need
(server + css watcher + js bundler) at the same time. That file
looks something like:
web: bin/rails server
css: bin/rails tailwindcss:watch
Further Information
-
The Rails guides page on the asset pipeline: https://guides.rubyonrails.org/asset_pipeline.html
-
The Propshaft README: https://github.com/rails/propshaft
-
The importmap-rails README: https://github.com/rails/importmap-rails