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:

app/assets/stylesheets/application.css
/*
 * 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:

app/javascript/application.js
// 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:

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:

Procfile.dev
web: bin/rails server
css: bin/rails tailwindcss:watch

Further Information