Sijin T V
Sijin T V A passionate Software Engineer who contributes to the wonders happenning on the internet

Propshaft Asset Pipeline: A Modern, Light Replacement for Sprockets

Sprockets was designed for the Rails era where the framework compiled CoffeeScript, SCSS, and ERB-templated JavaScript on the fly. That era is over. Modern browsers ship ES modules and CSS variables natively, and the bundling work has moved to esbuild, Vite, or import maps. What a production asset pipeline needs now is embarrassingly small: take files, compute a content digest, and hand the browser a URL that never changes until the file does. That’s exactly what Propshaft is, and it’s why Rails 7 made it the default for new apps in late 2021 while we kept shipping Sprockets out of inertia.

What Sprockets was doing wrong for us

The costs were all in the build path, not the request path. A Rails 6 app with roughly 400 assets (vendor JS, admin CSS, fonts, images) took about 90 seconds to assets:precompile in CI and peaked around 1.6 GB of memory while doing it. Most of those assets were static bytes that Sprockets re-processed — and re-compressed into gzip variants — on every deploy. In development, the first request after a file change triggered a per-asset on-demand compile that could stall a page for over a second. And because Sprockets keyed digests by logical path plus a manifest of its own making, we occasionally shipped a manifest that referenced a fingerprint that had silently regenerated, forcing a nuke-and-rebuild deploy. None of that machinery was buying us anything.

How Propshaft thinks about assets

Propshaft drops the concept of “processing” entirely. It keeps a flat load path — a list of directories it scans — and at build time produces a manifest mapping every logical path (like application.css) to a digested filename (like application-0a1b2c3d4e5f6a7b.css). The digest is a content hash, so identical files produce identical URLs across deploys, and since the URL changes whenever the bytes change, the response header public, max-age=31536000, immutable is safe for every served asset.

That’s the whole pipeline. No transpilation, no directives, no intermediate representations. If you need ES6-to-ES5 or JSX, a bundler does it before Propshaft ever sees the output; if you need gzip or brotli, your reverse proxy does it on the fly (Nginx with gzip_static or Kamal’s Thruster). Propshaft just moves bytes and answers asset_path lookups from the manifest.

The migration

For a standard app it’s a two-line change plus cleanup:

1
2
3
4
5
6
7
8
9
# Gemfile
# remove sprockets-rails (and sassc-rails if present), then:
gem "propshaft"

# config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join("node_modules")

# config/environments/production.rb
Rails.application.config.assets.compile = false

Views keep using stylesheet_link_tag, javascript_include_tag, image_tag, and font_url exactly as before — the helpers stay, the implementation underneath changes. assets:precompile now copies files and writes a small JSON manifest instead of running a full build graph.

The real migration blockers are never the helpers; they’re the Sprockets-specific features you’ve quietly depended on:

  • //= require directive comments in JavaScript stop working. Code must be resolved by the bundler or imported as ES modules.
  • .erb preprocessing inside app/assets disappears. Move anything templated into the view layer or the bundler.
  • sassc-rails and asset compression go away. Sprockets gzip variants were a win when proxies couldn’t compress well; they’re dead weight now.

Numbers from our move

On the same 400-asset codebase, assets:precompile dropped from ~90 seconds and 1.6 GB peak memory to about 4 seconds and negligible memory — it’s file copy plus a hash pass. CI asset caching became irrelevant. Dev boot stopped recompiling everything on first request; Propshaft builds the manifest in milliseconds and falls back to scanning on change. First-request latency for an uncached asset went from ~1.2 s to under 100 ms.

What we gave up, honestly

  • Digest lookups are manifest-driven in production. A file referenced in the manifest that isn’t on disk raises Propshaft::MissingAssetError at boot-check time instead of producing a 404 later — better, but it means you must deploy assets and app together or precompile in the image. Kamal-style deploys make this natural.
  • Third-party engines that still ship Sprockets directives (admin themes, older view-component gems) need attention. Keeping sprockets-rails installed so an engine can register its helpers while Propshaft owns your app assets works, but expect helper friction; the durable fix is vendoring the engine’s prebuilt JS/CSS and dropping Sprockets entirely.
  • Propshaft is not a bundler. If you rely on it to concatenate and minify your own JavaScript, you’ll get bare files. Put esbuild or import map handling in front of it.

The lesson that stuck: an asset pipeline should be boring. Sprockets kept growing features to solve problems browsers already solved; Propshaft recognized the pipeline’s job shrank to one job and removed everything else. If you’re still on Sprockets in 2025, the migration is a weekend’s work and the CI bill alone pays for it.

comments powered by Disqus