Migrating from Sprockets to Propshaft in Rails 8

Rails 8 introduces a significant change to the asset pipeline by making Propshaft the default asset pipeline, replacing Sprockets which has been the default since Rails 3.1.

This change reflects the modern approach to asset management in Rails applications.

What is Propshaft?

Propshaft is a lightweight asset pipeline that does one thing well: it fingerprints assets and serves them.

Unlike Sprockets which bundles, transpiles, and concatenates files, Propshaft delegates those responsibilities to specialized tools.

Here’s what each pipeline actually does:

Feature Sprockets Propshaft
Asset fingerprinting Yes Yes
Manifest directives (//= require) Yes No
Sass/SCSS compilation Yes No
CoffeeScript compilation Yes No
JavaScript bundling Yes No
Source maps Limited No

Propshaft expects us to use dedicated tools:

  • JavaScript: Import Maps, esbuild, or Webpack via jsbundling-rails
  • CSS: Tailwind, Sass via cssbundling-rails, or plain CSS

Migrating a Rails 7.1 app to Propshaft

Let’s walk through migrating a typical Rails 7.1 application that uses Bootstrap, jQuery, Select2, and Font Awesome.

Step 1: Audit current asset setup

Before migrating, we need to understand what our app uses:

# Find Sprockets directives

grep -r "//= require" app/assets/javascripts/
grep -r "*= require" app/assets/stylesheets/

# Check for preprocessed files

find app/assets -name "*.scss" -o -name "*.sass" -o -name "*.coffee"

Step 2: Update the Gemfile

# Remove Sprockets gems

gem "sprockets-rails"  # DELETE

gem "sassc-rails"      # DELETE if present

gem "jquery-rails"     # DELETE - we'll use npm instead

gem "bootstrap"        # DELETE - we'll use npm instead


# Add Propshaft

gem "propshaft"

# Add JavaScript bundling (required if using npm packages)

gem "jsbundling-rails"

# Add CSS bundling (ONLY if using Sass/SCSS files)

# Skip this if using plain CSS or Tailwind

gem "cssbundling-rails"

Run the installation:

bundle install
rails javascript:install:esbuild

If using Sass/SCSS files, also run:

rails css:install:sass

If using plain CSS, we can skip cssbundling-rails entirely. Propshaft serves .css files directly without any build step.

Step 3: Install npm packages

Replace gem-based assets with npm packages:

# Core libraries

yarn add @hotwired/turbo-rails @hotwired/stimulus

# Bootstrap (previously bootstrap gem)

yarn add bootstrap @popperjs/core

# jQuery (previously jquery-rails gem)

yarn add jquery

# Select2 (previously select2-rails gem)

yarn add select2

# Font Awesome (previously font-awesome-rails gem)

yarn add @fortawesome/fontawesome-free

Bootstrap migration

Before (Sprockets with bootstrap gem):

// app/assets/javascripts/application.js

//= require jquery

//= require popper

//= require bootstrap
// app/assets/stylesheets/application.scss

@import "bootstrap";

After (Propshaft with npm):

// app/javascript/application.js

import * as bootstrap from "bootstrap"

// Initialize tooltips globally

document.addEventListener("turbo:load", () => {
  const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
  tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el))
})
// app/assets/stylesheets/application.sass.scss

@import "bootstrap/scss/bootstrap";

jQuery migration

Before (Sprockets with jquery-rails gem):

// app/assets/javascripts/application.js

//= require jquery

//= require jquery_ujs


$(document).ready(function() {
  $(".dropdown").dropdown();
});

After (Propshaft with npm):

// app/javascript/application.js

import jquery from "jquery"
window.jQuery = jquery
window.$ = jquery

document.addEventListener("turbo:load", () => {
  $(".dropdown").dropdown()
})

Note: We expose jQuery globally because many plugins expect $ and jQuery on window.

Select2 migration

Before (Sprockets with select2-rails gem):

// app/assets/javascripts/application.js

//= require select2


$(document).ready(function() {
  $(".select2").select2();
});
// app/assets/stylesheets/application.scss

//= require select2

After (Propshaft with npm):

// app/javascript/application.js

import jquery from "jquery"
window.jQuery = jquery
window.$ = jquery

import select2 from "select2"
select2($)  // Initialize Select2 with jQuery


document.addEventListener("turbo:load", () => {
  $(".select2").select2({
    theme: "bootstrap-5"
  })
})
// app/assets/stylesheets/application.sass.scss

@import "select2/dist/css/select2.css";

Font Awesome migration

Before (Sprockets with font-awesome-rails gem):

// app/assets/stylesheets/application.scss

@import "font-awesome";

After (Propshaft with npm):

// app/assets/stylesheets/application.sass.scss

@import "@fortawesome/fontawesome-free/css/all.css";

For the webfonts, copy them to the assets folder or configure the path:

# Copy font files to assets

cp -r node_modules/@fortawesome/fontawesome-free/webfonts app/assets/

Or override the font path in Sass:

// app/assets/stylesheets/application.sass.scss

$fa-font-path: "../webfonts";
@import "@fortawesome/fontawesome-free/scss/fontawesome";
@import "@fortawesome/fontawesome-free/scss/solid";
@import "@fortawesome/fontawesome-free/scss/brands";

Stimulus controllers

Before (Sprockets):

// app/assets/javascripts/controllers/hello_controller.js

//= require stimulus


import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
    this.element.textContent = "Hello World!"
  }
}

After (Propshaft):

// app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.textContent = "Hello World!"
  }
}
// app/javascript/controllers/index.js

import { application } from "./application"
import HelloController from "./hello_controller"

application.register("hello", HelloController)

Handling gems without npm equivalents

Some Rails gems bundle JavaScript assets that don’t have npm packages. The cocoon gem for nested forms is a common example.

1. Vendor the JavaScript file

Copy the JavaScript from the gem into our app:

# Find and copy the gem's JavaScript

cp $(bundle show cocoon)/app/assets/javascripts/cocoon.js app/javascript/vendor/

Then import it in app/javascript/application.js:

import "./vendor/cocoon"

2. Replace with a Stimulus controller

Write a Stimulus controller that replicates the gem’s functionality. For cocoon, create a nested_form_controller.js that handles adding/removing nested fields. This is more work upfront but gives us full control and removes the gem dependency.

3. Use Import Maps (if gem supports it)

# config/importmap.rb

pin "cocoon", to: "cocoon.js", preload: true

This only works if the gem places JavaScript where Propshaft can find it.

Common gems and their alternatives:

Gem npm Package Alternative
jquery-rails jquery Use npm
bootstrap bootstrap Use npm
select2-rails select2 Use npm
cocoon None Vendor file or Stimulus
best_in_place None Stimulus controller
chartkick chartkick Use npm
flatpickr flatpickr Use npm
toastr-rails toastr Use npm

Handling CSS asset paths

Images in CSS

Before (Sprockets):

.hero {
  background-image: image-url("hero-bg.jpg");
}

.logo {
  background-image: asset-url("logo.png");
}

After (Propshaft):

.hero {
  background-image: url("hero-bg.jpg");
}

.logo {
  background-image: url("logo.png");
}

Propshaft resolves paths relative to the app/assets/images/ directory.

Custom fonts

Before (Sprockets):

@font-face {
  font-family: "CustomFont";
  src: font-url("custom-font.woff2") format("woff2");
}

After (Propshaft):

Place fonts in app/assets/fonts/ and reference them:

@font-face {
  font-family: "CustomFont";
  src: url("custom-font.woff2") format("woff2");
}

Update layout and configuration

Layout file

<%# app/views/layouts/application.html.erb %>
<!DOCTYPE html>
<html>
  <head>
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

Remove Sprockets configuration

Delete config/initializers/assets.rb and remove these from environment files:

# config/environments/production.rb

# DELETE these lines:

config.assets.compile = false
config.assets.js_compressor = :terser
config.assets.css_compressor = :sass

Build scripts

The installers create these automatically in package.json:

{
  "scripts": {
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets",
    "build:css": "sass ./app/assets/stylesheets/application.sass.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  }
}

Common errors and fixes

jQuery plugin not working

TypeError: $(...).select2 is not a function

Cause: jQuery not exposed globally.

Fix: Add to application.js:

import jquery from "jquery"
window.jQuery = jquery
window.$ = jquery

Bootstrap JavaScript not initializing

Uncaught TypeError: bootstrap.Tooltip is not a constructor

Cause: Bootstrap not imported correctly.

Fix: Import the full Bootstrap bundle:

import * as bootstrap from "bootstrap"
window.bootstrap = bootstrap

Font Awesome icons not showing

Cause: Webfont files not found.

Fix: Either copy webfonts to assets or use the CSS version:

// Use pre-built CSS instead of Sass

@import "@fortawesome/fontawesome-free/css/all.css";

Sass variables undefined

SassError: Undefined variable: "$primary"

Cause: Bootstrap variables not loaded before custom styles.

Fix: Import Bootstrap functions and variables first:

// app/assets/stylesheets/application.sass.scss

@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "variables";  // Our custom variables

@import "bootstrap/scss/bootstrap";

When to stay with Sprockets

Consider keeping Sprockets if:

  • Using CoffeeScript extensively: Requires rewriting all files to JavaScript.

  • Relying on asset gems without npm alternatives: Some older gems only work with Sprockets.

  • Complex custom Sprockets processors: Need to reimplement in build tools.

To use Sprockets in Rails 8:

rails new myapp --asset-pipeline=sprockets

Pros and cons

Pros:

  • 3-5x faster asset compilation in development
  • Access to full npm ecosystem
  • Modern ES modules instead of global scripts
  • Simpler debugging - source files served directly
  • Better alignment with frontend tooling

Cons:

  • Migration effort for existing apps
  • Need to learn npm/yarn workflow
  • Some gems don’t have npm equivalents
  • Additional build step for CSS/JS

Conclusion

Propshaft works best when we embrace the npm ecosystem. Most popular libraries like Bootstrap, jQuery, and Select2 have npm packages that work better than their gem counterparts.

For new apps, Propshaft with esbuild provides a modern, fast setup.

For existing apps, the migration is straightforward if we’re already using common libraries. The main work is replacing gem-based assets with npm packages.

References

Need expert help with Rails performance?

Saeloun is a Rails Foundation Contributing Member helping teams modernize, upgrade, scale, and maintain production Rails applications.

Our Expertise

  • Rails contributors
  • 500+ Technical Articles
  • Production Rails consulting
  • Performance Optimization

Services

  • Rails application development
  • Code Audits
  • Rails upgrades
  • Team Augmentation

Need help on your Ruby on Rails or React project?

Join Our Newsletter