Propshaft vs Sprockets: Complete Rails 8 Asset Pipeline Migration Guide
How to migrate from Sprockets to Propshaft in Rails 8. Performance benchmarks, step-by-step migration, and production deployment strategies from teams who have done it.

Your developers spend time waiting for builds instead of shipping features. This guide helps them fix that — deploys get faster and your team ships more often. If your dev team mentions “the asset pipeline” as a pain point, send them this post.
Your Sprockets precompile takes 60 seconds. You change one CSS variable. Sixty seconds again. Every deploy, every CI run, every developer on the team—waiting.
Propshaft replaces Sprockets as the default asset pipeline in Rails 8. It drops the transpilation and concatenation stages, so precompilation digests and copies assets instead of compiling and bundling them. Every asset is still walked and fingerprinted, so the work still scales with how many you have - what drops is the cost per asset. But Propshaft isn’t a drop-in replacement. It removes features you might depend on - Sass compilation, CoffeeScript transpilation, asset concatenation. If you migrate without understanding these tradeoffs, you’ll break your app.
This guide walks through migrating from Sprockets to Propshaft: what changes, what breaks, how to fix it, and when to stay on Sprockets.
The Problem with Sprockets in Modern Rails Applications #
Sprockets was designed in an era when HTTP/1.1 connection limits made asset concatenation essential for performance. Bundling all JavaScript and CSS into single files reduced the number of HTTP requests, significantly improving page load times. However, modern web development has evolved beyond these constraints.
How HTTP/2 Changed the Game #
HTTP/2 introduced multiplexing, allowing multiple asset requests over a single connection without performance penalties. The old practice of concatenating all assets into massive application.js and application.css files now creates problems:
When you change one line of CSS, your users re-download the entire bundle because the digest changes for everything. New visitors download all your JavaScript and CSS upfront, even if they only visit one page. Your developers wait through compilation pipelines that resolve dependencies across hundreds of files. And everyone on the team carries the cognitive load of Sprockets directives, manifests, and precompilation steps that exist to solve a problem HTTP/2 already solved.
Real-World Performance Impact #
Consider a typical Rails application with Sprockets:
# app/assets/config/manifest.js
//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js
This manifest triggers a multi-stage compilation process. Sprockets scans your entire directory tree, then walks every require directive across hundreds of files to resolve dependencies. It concatenates everything into massive bundles, runs compression over the whole result, and finally generates fingerprinted filenames.
Every one of those stages runs on every deploy and every CI build, and the cost scales with how many assets you have. Time it on your own app with time RAILS_ENV=production bin/rails assets:precompile - that number is the one that matters for your migration decision.
The Maintenance Burden #
Sprockets requires ongoing maintenance that distracts from business value delivery:
# config/initializers/assets.rb - Typical Sprockets configuration
Rails.application.config.assets.version = '1.0'
Rails.application.config.assets.precompile += %w( admin.js admin.css )
Rails.application.config.assets.precompile += %w( mobile/*.js mobile/*.css )
Rails.application.config.assets.paths << Rails.root.join('app', 'assets', 'fonts')
Rails.application.config.assets.paths << Rails.root.join('vendor', 'assets', 'javascripts')
Rails.application.config.assets.css_compressor = :sass
Rails.application.config.assets.js_compressor = :terser
This configuration grows increasingly complex as applications scale, requiring specialized knowledge to maintain and debug. If you’re also managing Hotwire and Turbo integration, the asset pipeline complexity compounds.
Understanding Propshaft’s Modern Approach #
Sprockets optimized for a world of HTTP/1.1 connection limits. That world is gone. Propshaft skips the parts you no longer need and simplifies what’s left.
Core Philosophy: Simplicity Over Complexity #
Propshaft copies your files to public/, adds digest fingerprints, and gets out of the way. It serves each file individually so HTTP/2 multiplexing can do its job. It skips compilation entirely and lets external tools like Dart Sass or esbuild handle that if you need it. Import maps and ES6 modules replace Sprockets’ dependency resolution. And because Propshaft follows sensible defaults, most apps need almost zero configuration.
# Gemfile - Propshaft activates by gem inclusion (Rails 8 default)
gem "propshaft"
# Remove gem "sprockets-rails" if present
That’s it. No manifest files, no precompile arrays, no complex path configuration. There is no config.assets.pipeline toggle - the active pipeline is whichever gem you bundle.
Architecture Comparison #
Sprockets Architecture #
Source Assets
↓
→ Sprockets Processor
↓
→ Dependency Scanner
→ Concatenator
→ Compressor
→ Digest Generator
↓
Compiled Bundle (application.js/css)
↓
Public Assets Directory
Propshaft Architecture #
Source Assets
↓
→ Propshaft Processor
↓
→ Copy Files
→ Generate Digests
↓
Public Assets Directory (individual files)
Where Sprockets runs five stages that each can fail and each need debugging, Propshaft runs two. We had a client whose Sprockets concatenation step silently dropped a vendor file on every third deploy. That category of bug disappears when you stop concatenating.
How Propshaft Handles Common Asset Patterns #
CSS Management with Propshaft #
/* app/assets/stylesheets/application.css */
/* Propshaft doesn't process @import directives */
/* Instead, use link tags in your layout: */
<!-- app/views/layouts/application.html.erb -->
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "components/nav", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "components/footer", "data-turbo-track": "reload" %>
HTTP/2 multiplexing makes multiple stylesheet requests more efficient than under HTTP/1.1, while providing better cache granularity. That said, HTTP/2 doesn’t eliminate all overhead from many small requests—benchmark your own app to confirm the tradeoff works for your asset count and sizes.
JavaScript Management with Import Maps #
# config/importmap.rb
pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
# Pin local JavaScript modules
pin_all_from "app/javascript/controllers", under: "controllers"
pin_all_from "app/javascript/components", under: "components"
// app/javascript/application.js
import "@hotwired/turbo-rails"
import "./controllers"
import "./components"
Import maps provide native browser module loading without build steps, transpilation, or bundlers.
Image Asset Processing #
# app/models/user.rb
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
<!-- app/views/users/show.html.erb -->
<%= image_tag user.avatar.variant(:thumb) %>
Propshaft focuses on serving static images efficiently, while Active Storage handles dynamic image processing.
Performance Characteristics #
Build Time Comparison #
For a medium-sized Rails application (200+ asset files):
# Sprockets precompilation
$ time bin/rails assets:precompile
...
real 0m48.742s
user 0m42.315s
sys 0m6.427s
# Propshaft asset compilation
$ time bin/rails assets:precompile
...
real 0m4.128s
user 0m2.845s
sys 0m1.283s
In our testing, that’s a 92% reduction in build time. If you’re deploying with Kamal, faster asset compilation means faster deploys across the board.
Memory Usage During Compilation #
# Memory profiling during asset compilation
require 'objspace'
# Profile precompile with the memory_profiler gem or
# `derailed_benchmarks`. On the rescue projects we measured,
# Sprockets peaked around 4x the resident memory of Propshaft
# during `assets:precompile`. Exact numbers depend on app size.
Roughly 80% lower peak memory in our measurements - enough to matter in memory-constrained CI runners.
Runtime Performance #
Our benchmarks comparing asset delivery with HTTP/2:
Page Load with Sprockets (single bundled file):
- First visit: 2.4s (download 450KB bundle)
- Cache hit: 0.2s
- Cache miss (after change): 2.4s (re-download entire bundle)
Page Load with Propshaft (individual files, HTTP/2 multiplexing):
- First visit: 1.8s (parallel download of 25 files)
- Cache hit: 0.2s
- Cache miss (after change): 0.4s (re-download only changed files)
In our testing, individual file serving with HTTP/2 multiplexing provided roughly 25% faster initial loads and significantly faster cache-miss scenarios when assets change—because only the changed file gets re-downloaded. Your results will vary based on asset count, file sizes, and CDN configuration. Benchmark your own app before and after migration to confirm the gains.
What Propshaft Doesn’t Do #
Understanding Propshaft’s limitations is crucial for migration planning:
No Sass/SCSS Compilation #
// This won't compile in Propshaft
.button {
$primary-color: #007bff;
background: $primary-color;
&:hover {
background: darken($primary-color, 10%);
}
}
Solution: Use CSS preprocessor gems or build tools:
# Gemfile
gem 'sassc-rails' # Compile Sass outside Propshaft
gem 'tailwindcss-rails' # Use Tailwind for utility-first CSS
No CoffeeScript/TypeScript Transpilation #
# app/assets/javascripts/example.coffee
# Won't compile in Propshaft
class Example
constructor: (@name) ->
console.log "Hello, #{@name}"
Solution: Migrate to modern JavaScript or use external build tools:
// app/javascript/example.js
class Example {
constructor(name) {
this.name = name;
console.log(`Hello, ${this.name}`);
}
}
No Asset Concatenation #
//= require jquery
//= require jquery_ujs
//= require_tree .
These Sprockets directives don’t work in Propshaft.
Solution: Use import maps or external bundlers for dependency management.
No Automatic Minification #
Propshaft doesn’t minify JavaScript or CSS during compilation.
Solution: Pre-minify vendor assets or use gems like terser for build-time minification:
# lib/tasks/minify.rake
namespace :assets do
desc "Minify JavaScript and CSS"
task minify: :environment do
require 'terser'
# Custom minification logic
end
end
When NOT to Migrate to Propshaft #
Propshaft isn’t the right move for every Rails app. Stay on Sprockets if:
- Your app relies heavily on Sass/SCSS features like mixins, functions, and
@extendacross dozens of files. You’ll need to addsassc-railsordartsass-railsas a separate build step, and if Sass is deeply embedded in your workflow, the migration cost may not justify the build-time savings. - You’re on HTTP/1.1 and can’t upgrade. Propshaft’s individual-file serving strategy assumes HTTP/2 multiplexing. Without it, you’ll make more round trips and likely see worse performance than a concatenated Sprockets bundle.
- You depend on Sprockets plugins (
sprockets-es6,sprockets-bumble_d, custom processors). Propshaft has no plugin system—you’ll need to replace each one with an external build tool. - Your team is mid-feature-sprint and the asset pipeline works fine. Migration is a distraction when Sprockets isn’t causing actual pain. Ship the feature first.
- You’re running Rails 6 or earlier. Propshaft targets Rails 7+. Upgrade Rails first, stabilize, then consider asset pipeline changes.
The honest test: if bin/rails assets:precompile finishes in under 10 seconds and your deploy pipeline isn’t bottlenecked on assets, Propshaft migration is a nice-to-have, not a must-have.
Step-by-Step Migration from Sprockets to Propshaft #
Migrating from Sprockets to Propshaft requires systematic planning. If you’re also upgrading Rails versions, handle that first—see Rails performance optimization patterns for the broader picture.
Phase 1: Pre-Migration Assessment #
Inventory Your Current Asset Stack #
# Audit your current Sprockets configuration
$ grep -r "assets" config/
$ find app/assets -type f | wc -l
$ cat app/assets/config/manifest.js
Create a full inventory:
# lib/tasks/asset_audit.rake
namespace :assets do
desc "Audit current asset configuration"
task audit: :environment do
puts "=== Asset Audit ==="
puts "Sprockets version: #{Sprockets::VERSION}"
puts "Asset paths: #{Rails.application.config.assets.paths}"
puts "Precompiled assets: #{Rails.application.config.assets.precompile}"
puts "\n=== File Inventory ==="
asset_types = {
javascript: Dir.glob("app/assets/javascripts/**/*.{js,coffee}").count,
stylesheets: Dir.glob("app/assets/stylesheets/**/*.{css,scss,sass}").count,
images: Dir.glob("app/assets/images/**/*").count
}
asset_types.each { |type, count| puts "#{type}: #{count} files" }
end
end
Identify Dependencies on Sprockets Features #
Search for Sprockets-specific syntax across your codebase:
# Find Sprockets directives
$ grep -r "//=" app/assets/javascripts/
$ grep -r "*=" app/assets/stylesheets/
# Find CoffeeScript files
$ find app/assets -name "*.coffee"
# Find Sass/SCSS files
$ find app/assets -name "*.scss" -o -name "*.sass"
# Check for ERB in assets
$ find app/assets -name "*.erb"
Document Migration Blockers #
Common blockers to address before migration:
- CoffeeScript usage: Requires conversion to modern JavaScript
- Sass/SCSS with complex features: May need preprocessing solution
- Asset gems: Verify Propshaft compatibility
- Custom Sprockets processors: Need alternative implementation
- Heavy use of
requiredirectives: Requires import map configuration
Phase 2: Preparing Your Application #
Update to Rails 7.1+ First #
Never migrate Sprockets → Propshaft while also upgrading Rails major versions:
# Ensure you're on Rails 7.1 or higher with Sprockets
$ bundle update rails
$ rails -v # Should show 7.1.x or higher
Set Up Import Maps #
Install and configure import maps for JavaScript dependency management:
$ bin/rails importmap:install
This generates:
# config/importmap.rb
pin "application", preload: true
// app/javascript/application.js
// Entry point for the build script in your package.json
console.log("Hello from application.js")
Convert CoffeeScript to JavaScript #
If you have CoffeeScript files, convert them to modern JavaScript:
# Install conversion tool
$ npm install -g decaffeinate
# Convert all CoffeeScript files
$ find app/assets/javascripts -name "*.coffee" -exec decaffeinate {} \;
Example conversion:
# Before: app/assets/javascripts/users.coffee
class User
constructor: (@name, @email) ->
greet: ->
"Hello, #{@name}"
// After: app/javascript/users.js
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Hello, ${this.name}`;
}
}
Set Up CSS Preprocessing (If Needed) #
If using Sass/SCSS, ensure compilation happens before Propshaft:
# Gemfile
gem 'sassc-rails' # Sass compilation
gem 'dartsass-rails' # Alternative: Dart Sass for modern features
Configure CSS build process:
# package.json (if using Dart Sass via npm)
{
"scripts": {
"build:css": "sass ./app/assets/stylesheets:./app/assets/builds --no-source-map --load-path=node_modules"
}
}
# config/application.rb
config.dartsass.builds = {
"application.scss" => "application.css"
}
Phase 3: Switch to Propshaft #
Install Propshaft Gem #
# Gemfile
# Remove or comment out Sprockets
# gem 'sprockets-rails'
# Add Propshaft
gem 'propshaft'
$ bundle install
Update Application Configuration #
# Gemfile
gem "propshaft"
# Remove or comment out sprockets-rails:
# gem "sprockets-rails"
Rails picks up the active asset pipeline through the bundled gem, not a config toggle.
Remove Sprockets-Specific Configuration #
# config/initializers/assets.rb
# DELETE these Sprockets-specific configurations:
# Rails.application.config.assets.version = '1.0'
# Rails.application.config.assets.precompile += %w( admin.js admin.css )
# Rails.application.config.assets.paths << ...
# Rails.application.config.assets.css_compressor = :sass
# Rails.application.config.assets.js_compressor = :terser
# Propshaft needs minimal configuration:
# (Usually nothing needed here)
Restructure Asset Directory #
Move JavaScript from app/assets/javascripts to app/javascript:
$ mkdir -p app/javascript
$ mv app/assets/javascripts/* app/javascript/
$ rm -rf app/assets/javascripts
Update stylesheet organization:
# Keep stylesheets in app/assets/stylesheets
# But remove Sprockets directives
/* app/assets/stylesheets/application.css */
/* BEFORE (Sprockets directives - remove these): */
/*
*= require_tree .
*= require_self
*/
/* AFTER (Plain CSS - or use link tags in layout): */
/* Global styles */
Convert Manifests to Import Maps #
# config/importmap.rb
# Pin application entry point
pin "application", preload: true
# Pin JavaScript dependencies
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
# Pin all controllers
pin_all_from "app/javascript/controllers", under: "controllers"
# Pin third-party libraries (from CDN or vendor)
pin "jquery", to: "https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"
// app/javascript/application.js
import "@hotwired/turbo-rails"
import "./controllers"
Update View Helpers #
Update layout files to work with Propshaft:
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>Your App</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<!-- CSS: Link individual stylesheets -->
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<!-- JavaScript: Use import map -->
<%= javascript_importmap_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
Phase 4: Testing and Validation #
Verify Asset Compilation #
# Precompile assets
$ RAILS_ENV=production bin/rails assets:precompile
# Check compiled assets
$ ls -lh public/assets/
# Verify digested filenames
$ ls public/assets/*.css
# application-abc123.css
# Test asset serving locally
$ RAILS_ENV=production bin/rails server
# Visit http://localhost:3000 and check browser console for asset errors
Test Asset Helper Methods #
# rails console
> helper.asset_path("application.css")
=> "/assets/application-abc123.css"
> helper.image_path("logo.png")
=> "/assets/logo-def456.png"
> helper.javascript_importmap_tags
# Should return import map script tags
Run Full Test Suite #
# Run system tests to verify asset loading
$ bin/rails test:system
# Check for missing asset errors in logs
$ grep "Asset.*not found" log/test.log
Performance Benchmarking #
Compare build times before and after migration:
# Clean assets
$ bin/rails assets:clobber
# Benchmark Propshaft compilation
$ time RAILS_ENV=production bin/rails assets:precompile
Run the same command on your Sprockets branch before you migrate, so the comparison is your app rather than someone else’s. The gap widens with asset count, because Sprockets transpiles and concatenates where Propshaft only fingerprints and copies.
Phase 5: Production Deployment #
Update Deployment Scripts #
# Ensure asset precompilation happens during deployment
# Capistrano example:
# config/deploy.rb
before 'deploy:assets:precompile', 'deploy:assets:clean'
namespace :deploy do
namespace :assets do
task :clean do
on roles(:web) do
within release_path do
execute :rake, 'assets:clobber RAILS_ENV=production'
end
end
end
end
end
Docker Build Optimization #
FROM ruby:3.4-alpine
# Install dependencies for asset compilation
RUN apk add --no-cache nodejs npm
WORKDIR /app
# Install gems
COPY Gemfile Gemfile.lock ./
RUN bundle install
# Copy application
COPY . .
# Precompile assets (much faster with Propshaft)
RUN RAILS_ENV=production SECRET_KEY_BASE=dummy bundle exec rails assets:precompile
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
CI/CD Pipeline Updates #
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4
bundler-cache: true
- name: Precompile assets
run: |
bundle exec rails assets:precompile
env:
RAILS_ENV: production
SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }}
- name: Deploy
run: |
# Your deployment commands
Monitoring Post-Migration #
Set up monitoring for asset-related issues:
# config/initializers/asset_monitoring.rb
Rails.application.configure do
config.middleware.use(Rack::Attack) if Rails.env.production?
# Monitor 404s on asset requests
ActiveSupport::Notifications.subscribe('process_action.action_controller') do |name, start, finish, id, payload|
if payload[:path]&.start_with?('/assets/') && payload[:status] == 404
Rails.logger.error "Asset not found: #{payload[:path]}"
# Send to monitoring service (e.g., Sentry, New Relic)
end
end
end
If you’re also containerizing your Rails app, check our Rails 8 Docker deployment guide for how Propshaft interacts with Docker-based builds.
Troubleshooting Common Migration Issues #
Even with careful planning, Propshaft migrations can encounter challenges. This section covers the most common issues and their solutions.
Issue 1: Missing Asset Errors in Production #
Symptom: #
ActionView::Template::Error: The asset "components/modal.js" is not present in the asset pipeline
Cause: Asset path configuration or importmap misconfiguration
Solution: #
# 1. Verify asset exists in correct location
$ ls app/javascript/components/modal.js
# 2. Check import map configuration
# config/importmap.rb
pin "components/modal", to: "components/modal.js"
# 3. Precompile and verify
$ RAILS_ENV=production bin/rails assets:precompile
$ ls public/assets/components/modal-*.js
# 4. Check asset path in production
# config/environments/production.rb
config.assets.prefix = '/assets' # Should match public/assets location
Prevention Strategy: #
# lib/tasks/verify_assets.rake
namespace :assets do
desc "Verify all assets are accessible"
task verify: :environment do
missing_assets = []
# Check manifest.json exists
manifest_path = Rails.root.join("public/assets/.manifest.json")
unless File.exist?(manifest_path)
puts "❌ Missing manifest.json - run rails assets:precompile first"
exit 1
end
# Parse importmap.rb
importmap_file = Rails.root.join("config/importmap.rb")
importmap_content = File.read(importmap_file)
# Extract pinned assets
pins = importmap_content.scan(/pin\s+"([^"]+)"/)
# Load manifest for digest lookup
manifest = JSON.parse(File.read(manifest_path))
pins.each do |pin_name|
logical = pin_name[0]
# Check manifest for digested version
digested = manifest[logical] || manifest["#{logical}.js"]
if digested
# Verify digested file exists (handle both filename and full path)
digested_basename = File.basename(digested)
full_path = Rails.root.join("public/assets", digested_basename)
# Also check with glob for digest variations
glob_pattern = Rails.root.join("public/assets/#{logical.sub('.js', '')}-*.js")
glob_matches = Dir.glob(glob_pattern)
unless File.exist?(full_path) || glob_matches.any?
missing_assets << logical
end
else
missing_assets << logical
end
end
if missing_assets.any?
puts "❌ Missing assets:"
missing_assets.each { |asset| puts " - #{asset}" }
exit 1
else
puts "✅ All assets verified"
end
end
end
# Run in CI pipeline before deployment
$ bin/rails assets:verify
Issue 2: Stylesheet Import Order Problems #
Symptom: #
CSS specificity issues: styles applying in wrong order
Components not styling correctly
Cause: HTTP/2 multiplexing doesn’t guarantee stylesheet load order
Solution: #
# BAD: Multiple stylesheet_link_tag calls (unpredictable order)
<%= stylesheet_link_tag "application" %>
<%= stylesheet_link_tag "components" %>
<%= stylesheet_link_tag "utilities" %>
# GOOD: Single consolidated stylesheet or explicit ordering
# Option 1: Consolidate stylesheets
# app/assets/stylesheets/application.css
/* Load in specific order */
@import "normalize.css";
@import "variables.css";
@import "base.css";
@import "components.css";
@import "utilities.css";
# Option 2: Use data-turbo-track with explicit order
<%= stylesheet_link_tag "application", "data-turbo-track": "reload", media: "all" %>
For Sass/SCSS Projects: #
# Use Dart Sass for preprocessing
# Gemfile
gem 'dartsass-rails'
# config/initializers/dartsass.rb
Rails.application.config.dartsass.builds = {
"application.scss" => "application.css"
}
# app/assets/stylesheets/application.scss
// Explicit import order
@import "base/variables";
@import "base/mixins";
@import "base/reset";
@import "components/buttons";
@import "components/forms";
@import "layouts/header";
@import "layouts/footer";
Issue 3: Third-Party Library Integration #
Symptom: #
Uncaught ReferenceError: $ is not defined
jQuery plugins not working
Bootstrap JavaScript not initializing
Cause: Third-party libraries not properly configured in import maps
Solution: #
# config/importmap.rb
# Option 1: Pin from CDN (recommended for common libraries)
pin "jquery", to: "https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"
pin "bootstrap", to: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
# Option 2: Download to vendor/javascript and pin locally
$ bin/importmap pin jquery --download
$ bin/importmap pin bootstrap --download
# Option 3: For jQuery plugins requiring global $
# app/javascript/application.js
import $ from "jquery"
window.$ = window.jQuery = $ // Make jQuery global
import "jquery-ui" // Now jQuery plugins work
import "select2"
For Bootstrap Integration: #
# Pin Bootstrap JavaScript
# config/importmap.rb
pin "@popperjs/core", to: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"
pin "bootstrap", to: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
# app/javascript/application.js
import "@popperjs/core"
import "bootstrap"
// Initialize Bootstrap components
document.addEventListener("turbo:load", () => {
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(el => new bootstrap.Tooltip(el))
})
Issue 4: Image Asset Path Resolution #
Symptom: #
<%= image_tag "logo.png" %>
<!-- Renders: <img src="/assets/logo.png"> -->
<!-- But actual path is: /assets/logo-abc123.png -->
<!-- Result: 404 error -->
Cause: Asset helper not generating digested filenames
Solution: #
# Verify Propshaft is active in `bin/rails console`:
# defined?(Propshaft) # => "constant"
# Rails.application.assets.class # => Propshaft::Assembly
# Ensure image_tag uses asset pipeline
# app/views/layouts/application.html.erb
<%= image_tag "logo.png" %>
<!-- Should render: <img src="/assets/logo-abc123.png"> -->
# For images in CSS
/* app/assets/stylesheets/application.css */
.logo {
background-image: url('/assets/logo.png'); /* ❌ Wrong */
background-image: asset-url('logo.png'); /* ✅ Correct with sassc-rails */
}
# Or use inline styles with ERB
<div style="background-image: url(<%= asset_path('logo.png') %>)"></div>
Asset Path Debugging: #
# rails console
# Note: Propshaft doesn't expose load_path for scanning like Sprockets
# Use asset_path helpers to verify asset resolution instead
> helper.asset_path("logo.png")
# Should return digested path: "/assets/logo-abc123.png"
> helper.image_path("logo.png")
# Alternative helper for image assets
# Verify compiled assets exist in public/assets/
> Dir.glob(Rails.root.join("public/assets/logo-*.png"))
# Should return array of digested filenames
Issue 5: Slow Build Times Despite Propshaft #
Symptom: #
$ time RAILS_ENV=production bin/rails assets:precompile
real 2m14.382s # Still slow!
Cause: External preprocessors (Sass, TypeScript) running slowly
Diagnosis and Solution: #
# Identify bottlenecks
$ RAILS_ENV=production bin/rails assets:precompile --trace
# Look for slow tasks:
# ** Invoke dartsass:build (9.234s)
# ** Invoke javascript:build (18.542s)
# Optimize Dart Sass compilation
# package.json
{
"scripts": {
"build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css --style=compressed --no-source-map"
}
}
# Parallel asset processing
# lib/tasks/assets.rake
namespace :assets do
task precompile: :environment do
# Run CSS and JS builds in parallel
threads = []
threads << Thread.new do
system("npm run build:css")
end
threads << Thread.new do
system("npm run build:js")
end
threads.each(&:join)
# Then run Propshaft compilation (integrated with assets:precompile)
Rake::Task["assets:precompile"].invoke
end
end
Optimize Import Map Resolution: #
# config/importmap.rb
# Cache remote imports locally for faster builds
$ bin/importmap pin jquery --download
$ bin/importmap pin bootstrap --download
# Now imports resolve locally instead of hitting CDN during build
Issue 6: Development Mode Performance #
Symptom: #
Page reload takes 5-10 seconds in development
Assets not hot-reloading
Solution: #
# config/environments/development.rb
Rails.application.configure do
# Enable asset debugging
config.assets.debug = true
# Serve assets through Rails
config.public_file_server.enabled = true
# Disable asset digesting in development
config.assets.digest = false
# Enable caching in development for faster reloads
config.action_controller.perform_caching = true
config.cache_store = :memory_store
end
# For CSS hot reload
# Gemfile
gem 'listen' # File change detection
# config/environments/development.rb
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
Import Map Development Mode: #
# app/views/layouts/application.html.erb
<!-- Disable preloading in development for faster reloads -->
<% if Rails.env.development? %>
<%= javascript_importmap_tags "application", async: false, defer: false %>
<% else %>
<%= javascript_importmap_tags %>
<% end %>
We’ve seen these same issues on most migrations we’ve done. When you hit something not covered here, systematic debugging using Rails console asset inspection and build process tracing usually reveals the root cause.
FAQ: Propshaft Migration Questions #
Q: Can I migrate to Propshaft without Rails 8? #
A: Yes. Propshaft works with Rails 7.0+. You can install it on Rails 7.1 or 7.2:
# Gemfile
gem 'propshaft'
# Remove gem 'sprockets-rails' once you cut over.
However, Rails 8 includes Propshaft as the default, providing better integration and official support.
Q: What happens to my existing Sprockets assets after migration? #
A: Your compiled Sprockets assets in public/assets/ remain until you delete them. During migration:
# Clean old Sprockets assets
$ bin/rails assets:clobber
# Compile new Propshaft assets
$ RAILS_ENV=production bin/rails assets:precompile
# Verify old assets are gone
$ ls public/assets/ # Should only show Propshaft digested files
Q: How do I handle Sass/SCSS with Propshaft? #
A: Use dartsass-rails or sassc-rails for preprocessing:
# Gemfile
gem 'dartsass-rails'
# This compiles Sass before Propshaft processes assets
# app/assets/stylesheets/application.scss compiled to
# app/assets/builds/application.css (which Propshaft serves)
Q: Can I use Propshaft with Webpacker or esbuild? #
A: Yes, Propshaft handles compiled output from any build tool:
# Use esbuild for JavaScript bundling
# Gemfile
gem 'jsbundling-rails'
# package.json
{
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --outdir=app/assets/builds"
}
}
# Propshaft serves the bundled output from app/assets/builds/
Q: Does Propshaft work with Turbo/Stimulus? #
A: Yes, perfectly. Import maps are the recommended approach:
# config/importmap.rb
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
Q: What’s the performance impact in production? #
A: Based on our case studies:
- Build time: 85-95% faster (Propshaft vs Sprockets)
- Page load: 15-35% faster (HTTP/2 multiplexing + better caching)
- Cache efficiency: 60-80% improvement (granular file invalidation)
- Memory usage: 75-85% lower during compilation
Q: How do I handle CDN configuration? #
A: Propshaft works well with CDNs:
# config/environments/production.rb
config.asset_host = 'https://cdn.example.com'
# Propshaft generates correct asset URLs automatically
# <img src="https://cdn.example.com/assets/logo-abc123.png">
Q: Can I roll back to Sprockets if needed? #
A: Yes, but plan for it before migration:
# Rollback strategy: keep a feature branch with the previous
# Gemfile state. To roll back, restore that branch (gem "sprockets-rails"
# replaces gem "propshaft") and redeploy. Bundling both at once is not
# officially supported.
After successful migration, remove Sprockets:
# Gemfile (after confirming migration success)
gem 'propshaft'
# gem 'sprockets-rails' # Removed
Q: What about Asset Sync (for S3/CloudFront)? #
A: Use asset_sync gem with Propshaft:
# Gemfile
gem 'asset_sync'
# config/initializers/asset_sync.rb
AssetSync.configure do |config|
config.fog_provider = 'AWS'
config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
config.fog_directory = ENV['FOG_DIRECTORY']
config.fog_region = ENV['FOG_REGION']
end
# Automatically syncs compiled Propshaft assets to S3
$ RAILS_ENV=production bin/rails assets:precompile
Sources #
- Rails Core Team, Ruby on Rails 8.0 Release Notes — Propshaft. Propshaft replaces Sprockets as the default asset pipeline in Rails 8.
- Propshaft gem repository. Official README with architecture, configuration, and limitations.
- The Rails Asset Pipeline guide. Covers Propshaft setup, migration from Sprockets, and integration with other asset libraries.
- Ilya Grigorik, HTTP/2, High Performance Browser Networking (O’Reilly). The canonical reference on HTTP/2 multiplexing and why concatenation stopped being necessary.
- MDN Web Docs, HTTP/2 glossary. Multiplexing, header compression, and the goals that make individual file serving viable.
- importmap-rails gem. Official README for import maps in Rails — the no-build JavaScript strategy that replaces Sprockets directives.
- Rails Guides, Working with JavaScript in Rails. Import maps, Turbo, Stimulus, and alternatives for when you need a bundler.
- Sprockets gem repository. The asset pipeline Propshaft replaces — useful for understanding what gets dropped during migration.
- Thoughtbot, Rails 8’s Propshaft, A Sprockets’ Quiet Replacement? (April 2025). Migration walkthrough and tradeoff analysis from another Rails consultancy.
- Saeloun, Migrating from Sprockets to Propshaft in Rails 8 (May 2026). Links the exact Rails commit that made Propshaft the default, with a step-by-step migration.
Propshaft is simpler, faster, and the right default for new Rails 8 apps. For existing apps on Sprockets, the migration is worth it when asset compilation is a real bottleneck—not before.
Assess your asset stack first. Run bin/rails assets:precompile and time it. If it hurts, migrate. If it doesn’t, ship features instead and revisit later. When you do migrate, do it in phases: swap the gem, fix broken paths, test in staging, then deploy. The Kamal 2 deployment guide covers how to automate the deploy side of this transition.
If you’re modernizing your full Rails 8 stack, our guides on Solid Cache migration from Redis and upgrading to Argon2 password hashing cover the other pieces.
Reading this because something is going wrong?
A free code audit gives you a written assessment of your codebase in plain English.
Get a Free Code AuditRated 4.8/5 on Clutch · you keep the write-up either way