Making Sense of Rails Assets

The Rails asset ecosystem is at peak complexity as it transitions from Sprockets to Importmaps and Propshaft, by way of Webpacker. How does it affect people who build Rails apps? How should Rails plugin developers navigate the transition?

Rails has always prided itself on convention over configuration and as a result, the community favors having one way to solve a problem vs. a bunch of different ways where a choice has to be made. That's why the state of the asset pipeline is so surprising—there's a lot of different ways to do it and a lot of different decisions that have to be made.

Today in Rails 7

Here's an over-simplified menu of your choices for an asset pipeline when creating a new Rails 7 application today:

Basic Asset Pipeline

Running rails new as of Rails 7 will default to bundling the importmap-rails gem for JavaScript and the sprockets gem for CSS and image fingerprinting. If you're building a Hotwire app or a simple Rails app that only requires a few JavaScript files, this approach is great since it doesn't require npm, yarn, or any other parts of a typical JavaScript toolchain.

This approach does require HTTP/2 to be performant. Why? Because HTTP/2 allows websites to serve up separate files over a single connection.

Compare that to HTTP/1, which requires several connections to serve up multiple files. Creating connections to servers that are located in galaxies far far away has overhead, which is why Sprockets concatenated all JavaScript and CSS files into one big file. It reduced the number of connections that needed to be made to those distant servers.

There are some popular Rails web hosts out there that don't support HTTP/2, so be sure to check with your host for support. Fly supports HTTP/2 out of the box, so no additional configuration is required to use import maps.

Complex Asset Pipeline

If your project has a more complicated frontend that requires JavaScript or CSS compilation or manages dependencies with yarn or npm, you'll want to use the jsbundling-rails and cssbundling-rails gems. Each of those gems brings in more complexity, which is only worth it if you absolutely need it.

But Wait, There's More!

If you're new to Rails, you should pick the Basic Asset Pipeline option and get back to shipping. Don't get caught up in analysis paralysis on the perfect asset pipeline because it doesn't exist.

But just for fun, and to prove a point about how confusing this can be, know that there's a few other options for deploying Rails that work for more specialized needs:

  • Tailwind asset pipeline - The tailwindcss-rails gem wraps the tailwindcss CLI, and is a great option if you plan to use only Tailwind and know you won't need to compile or manage other CSS dependencies.
  • Dart Sass asset pipeline - Like the Tailwind asset pipeline above, the dartsass-rails gem wraps the dart-sass binary and is great if you're certain that's all your project needs.
  • Sprockets-only asset pipeline - If you're coming into Rails 7 with an application that's using Sprockets to manage JavaScript assets, nothing says you have to change that. Sprockets will keep on chugging.
  • Webpacker asset pipeline - Rails 5.2 shipped with Webpacker, a wrapper for webpack, that made it possible for Rails apps to ship complex JS front-ends, like React, out of the box. While this gem will work in Rails 7, its been retired and is recommended to migrate to jsbundling-rails.

Pain Points

If you're coming into Rails 7 from an older Rails app, there are few pain points that you can expect to run into:

Migrating Pipelines

The documentation and tools for migrating pipelines isn't fully baked yet, though efforts are being made to make this clearer, as evident in a Rails forum post and Draft Github PR that updates the Asset Pipeline Guide. Improving documentation will help eliminate some of the confusion around the asset pipeline.

Another thing to think about: if you create a new Rails 7 app with the basic pipeline, then find out in the future that you need to switch to jsbundling-rails, you'll have to manually move around your asset files and reconfigure a few things.

Running the Development Server

For a long time, the way to run a Rails development server was bin/rails server . Some asset pipeline configurations require running development servers in a separate process from the Rails development server. There is a slight learning curve for Rails developers who have to switch from running bin/rails server to bin/dev and learn about Procfile.

Difficulty Using Assets From Rails Plugins

Where you're probably running into issues today is how to get assets from Rails plugins, like local_time, working in Rails. Why is that? Let's take a closer look starting with the Installation section of the project's README file:

## Installation

1. Add `gem 'local_time'` to your Gemfile.
2. Include `local-time.js` in your application's JavaScript bundle.

    Using the asset pipeline:
    ```js
    //= require local-time
    ```
    Using the [local-time npm package](https://www.npmjs.com/package/local-time):
    ```js
    import LocalTime from "local-time"
    LocalTime.start()
    ```

The first direction for inclusion in the asset bundle is Sprockets syntax. Since Sprockets managed load paths, it knew that //= require local-time should look in that project's lib/assets/javascripts/src path to find the asset.

The second direction for inclusion makes life a little more difficult for Rails plugin developers. In addition to maintaining the Ruby gem, they also now have to maintain an npm package and version that separately from the gem.

The third direction is missing, which would instruct Rails 7 application developers on how to pin the local-time npm package via bin/importmap pin. For the Rails plugin developer, there's not much direction on if they should distribute their JavaScript assets with the Rails gem or separately as an npm package.

This confusion exists today because of the different asset management systems that Rails currently has in play: sprockets, webpacker, jsbundling-rails, and importmaps. There's no One Way™ for third party gem developers to integrate their assets with Rails.

Fortunately work is being done by the Rails community to solve a few of these pain points.

The Future

In the future, Propshaft will serve as the interface between Rails applications and assets. It will replace all that Sprockets does and focus on these four things:

  1. Load paths - Propshaft will keep track of asset load paths from the app and Rails gem plugins. This should make installing Rails plugin gems much easier since they'll register with Propshaft, eliminating the guesswork needed to find assets.
  2. Digest stamping - Assets defined in the load paths will be digested and copied into the ./public directory, along with a manifest.json file, so Rails can serve up assets with long cache expiries. This is a well known technique that makes web applications load faster.
  3. Development server - Propshaft will run a development server inside of Rails, which means it will be possible to run some development environments via bin/rails server without a Procfile. This is still under active development, so expect changes in the final implementation.
  4. Basic compilers - Propshaft will support basic compilation steps, like replacing url(asset) strings in CSS files with url(digested-asset). Complex compilations will be delegated out to tools better suited for that purpose.

Rails will continue recommending importmaps for JavaScript assets when running rails new, but instead of including Sprockets, it will include Propshaft. Sprockets will probably continue being supported for a while, but it will be recommended to switch to Propshaft as the way to manage Rails assets.


The Past

You might be thinking, "how did the Rails asset pipeline get to be such a beast?". Like most questions that end with, "get to be such a beast", it's a long story.

Rails has been around for over a decade, a time long ago before HTTP/2 existed and the phrase "Backend JavaScript developer" was an oxymoron. Come on a journey with me and explore its past.

The Time Before Asset Management

When Rails first started, HTTP/2 didn't exist, JavaScript development as a full-time job was a foreign concept, and asset management was a matter of throwing Scriptaculus, Prototype, and an application.js file into the ./public/assets directory. No fingerprinting was necessary because there were so few moving parts.

There were third-party gems, like jammit, that were available for production websites that cared about asset efficiency, but for the most part it's something Rails devs didn't think about.

Life was good, then JavaScript file sizes starting getting bigger and became a much bigger concern of web application development.

Hello, My Name Is Sprockets

A few years later, when we swore off Prototype because it extended core JavaScript prototypes and moved on to jQuery, Sprockets came onto the scene. Most of us remember Sprockets as a huge pain, but it was actually pretty good when it arrived into Rails because source maps, transpiling, and all the other fancy stuff that JS compilers do wasn't that pervasive.

Move Over Sprockets, Webpacker Is in Town

At some point the JavaScript community flew past Sprockets in terms of pipeline sophistication. Rails developers envied source maps, tree shaking, and all sorts of other goodies that made for more efficient asset delivery. Sprockets couldn't keep up, so Webpacker was born.

Remember, this was all before Turbo landed. The way to build a high budget compelling Rails app at the time was to deploy a front-end Framework like React, Vue, or Backbone.js.

Hmm, Maybe Rails Apps Shouldn't Run So Much JavaScript and HTTP/2 Solves Some Asset Pipeline Problems

At some point a few folks in the Rails community had the revelation that fast, responsive web applications that developers typically associate with JavaScript-rich applications could be built without all the complexity of a single-page JavaScript application that talks to its server via an API. Turbo was born!

Turbo has nothing to do with the asset pipeline, so how is it relevant? Most Turbo apps require a few JavaScript files that don't need any fancy compiler technology, so it doesn't need a complex JavaScript compilation process. Turbo accomplishes this feat by keeping application state on the server, rendering HTML fragments, and delivering it over the wire to the browser where it swaps out parts of the DOM.

At the same time it was realized that HTTP/2 could deliver a lot of small asset files without incurring the performance penalties imposed by HTTP/1, so importmaps was born.

These two ideas came together in Rails 7 and gave us a much simpler way to build web applications without the complexity associated with front-end heavy JavaScript applications.