Bridging the Gap: Integrating Modern HMR into Old Rails Apps
In the early days of photography, subjects had to sit perfectly still for minutes at a time while the photographic plate was exposed. Any movement would result in a blur, ruining the image and requiring the entire process to start over. It was a deliberate process that demanded immense patience.
Similarly, developing frontend interfaces in older Ruby on Rails applications often feels like exposing an early photograph. You make a small change to a CSS class or a React component, save the file, and then wait for the entire page to reload. In doing so, you lose your scroll position, any data entered into complex forms, and the current state of your UI components. It is a workflow that demands patience, disrupts concentration, and ultimately costs engineering teams significant time in lost productivity.
Hot Module Replacement (HMR) is a feature of modern JavaScript development servers that allows modules to be updated in the browser at runtime without requiring a full page refresh. When a developer saves a file, the development server sends the updated module to the browser via WebSockets. The browser then swaps out the old module for the new one, preserving the current state of the application1.
Before we get into that, though, let’s take a step back and examine the structural reality of frontend tooling in legacy Rails applications.
The Structural Reality of Legacy Frontend Tooling
Historically, Rails applications relied on Sprockets – the Asset Pipeline – to compile, minify, and serve JavaScript and CSS. Sprockets is reliable and straightforward to use. However, it was designed in an era before modern JavaScript frameworks and HMR existed. It fundamentally operates by concatenating files and requires a full page reload to reflect changes2.
Later, Rails introduced Webpacker to bridge the gap with the NPM ecosystem. While Webpacker supported HMR, its complex configuration and slow compilation times frustrated developers. Today, Webpacker is officially retired, leaving many legacy applications in a precarious position regarding their frontend tooling31.
Three Paths to Modernization
When engineering teams decide to modernize their frontend workflow, they typically face three major approaches. Depending on the particular circumstances of your application, one of them may be more useful than the other two.
Shakapacker (The Webpacker Successor)
Shakapacker is the community-maintained successor to Webpacker.4
- Pros: If your application is already deeply entangled with Webpacker, upgrading to Shakapacker is the path of least resistance. It supports HMR and maintains a similar configuration structure.
- Cons: It still relies on Webpack under the hood, which can be memory-intensive and slower to compile than newer build tools.
jsbundling-rails (The Rails Default)
Introduced in Rails 7, jsbundling-rails provides a lightweight wrapper around modern bundlers like esbuild, rollup, or Webpack.
- Pros: It aligns with the current Rails “omakase” philosophy, keeping the Ruby side minimal2.
- Cons: Strictly speaking, when using esbuild with
jsbundling-rails, you typically get live reloading rather than true HMR. The browser still refreshes the page automatically when files change, which means application state is lost536.
Vite Ruby
Vite Ruby integrates the Vite build tool into Rails applications. Generally speaking, this is my preferred method for greenfield and legacy apps alike.
- Pros: Vite uses native ES modules during development, meaning it doesn’t need to bundle your entire application before starting the dev server. It provides true HMR for JavaScript, CSS, and frameworks like Vue, React, or Svelte17.
- Cons: It introduces a new tool to your stack and requires running a separate Vite development server alongside your Rails server3.
A Practical Walkthrough: Integrating Vite Ruby
Let’s illustrate how to integrate Vite Ruby into an older Rails application. This walkthrough assumes you have a standard Rails application that currently relies on Sprockets or an outdated version of Webpacker.
Step 1:Installation
First, we add the vite_rails gem to our Gemfile:
$ bundle add vite_rails
Next, we run the installation generator. This command will scaffold the necessary configuration files and update your package.json.8
$ bundle exec vite install
When you run this command, you will see output similar to the following:
Creating binstub
...snip...
Installing frontend dependencies
...snip...
Vite Ruby successfully installed!
We can verify that the installation succeeded by checking for the presence of the vite.json configuration file and the app/frontend directory. We can notice a few things here: Vite Ruby expects your modern frontend code to live in app/frontend by default, separating it cleanly from your traditional app/assets.9
Step 2:Configuration and Entrypoints
Vite creates an entrypoint file at app/frontend/entrypoints/application.js.
Let’s put the following code into app/frontend/entrypoints/application.js to demonstrate HMR with a basic CSS import:
import './application.css'
console.log('Vite with HMR is active!')
And in app/frontend/entrypoints/application.css:
body {
background-color: #f0f8ff; /* Alice Blue */
}
Step 3:Updating the Layout
To serve these files, we need to update our Rails layout. Open app/views/layouts/application.html.erb and add the Vite tag helpers.
<head>
<!-- Keep your existing Sprockets tags if needed -->
<%= stylesheet_link_tag 'application', media: 'all' %>
<!-- Add the Vite tags -->
<%= vite_client_tag %>
<%= vite_javascript_tag 'application' %>
</head>
Note the use of <%= vite_client_tag %>. This specific tag is crucial – it injects the WebSocket client that listens for HMR updates from the Vite development server9.
Step 4:Running the Servers
To benefit from HMR, we must run both the Rails server and the Vite development server. The vite install command creates a bin/vite executable for running the Vite development server. To run both servers concurrently, developers typically:8
-
Create a Procfile:
web: bundle exec rails s vite: bin/vite devThen run:
foreman start -
Or create a custom
bin/devscript:#!/usr/bin/env ruby system('bundle exec rails s') || system('bin/vite dev')
When you load your application in the browser, you will see the Alice Blue background. If you change the background color in application.css and save the file, the browser will update almost immediately – without a full page reload.
Addressing Complexity: Gradual Migration
One may wonder: if we integrate Vite, do we have to rewrite all our existing Sprockets assets immediately?
You do not.
You can run Vite Ruby alongside the traditional Asset Pipeline.9 This is a significant advantage for large legacy applications. Teams can adopt Vite for new features or gradually migrate existing components, reducing the risk associated with a complete rewrite. You can keep your legacy jQuery code in Sprockets while building new interactive forms with React and Vite.
Warnings and Caveats
Of course, introducing a new build system is not without risks.
Before you commit to Vite Ruby, be aware that you are introducing another moving part to your deployment process. In production, Vite compiles your assets into static files, much like Webpacker did. You must ensure your CI/CD pipeline is updated to run the Vite compilation step – typically bundle exec rails assets:precompile, which Vite hooks into10.
Additionally, if your application runs in a highly constrained Docker environment for development, configuring the HMR WebSocket connection can occasionally require manual port mapping adjustments. The Vite server typically runs on port 3036, and the browser must be able to reach that port directly11.
Conclusion
Integrating Hot Module Replacement into a legacy Rails application represents a fundamental shift in developer experience. By adopting tools like Vite Ruby, engineering teams can escape the slow cycle of full-page reloads, preserving application state and maintaining their flow.
While the transition requires careful configuration and an understanding of the underlying build processes, the compounding dividends in developer productivity make it a highly pragmatic investment for the long-term sustainability of the application179.
Footnotes
-
“Hot Module Replacement,” Webpack, accessed March 21, 2026, https://webpack.js.org/concepts/hot-module-replacement/ ↩ ↩2 ↩3
-
“The Asset Pipeline,” Ruby on Rails Guides, accessed March 21, 2026, https://guides.rubyonrails.org/asset_pipeline.html ↩
-
“HMR API,” Vite, accessed March 21, 2026, https://vite.dev/guide/api-hmr ↩ ↩2
-
“Shakapacker 7,” Shakacode, August 30, 2023, https://shakacode.com/blog/shakapacker-7/.\n[^17]: “Vite Ruby,” GitHub repository, accessed March 21, 2026, https://github.com/ElMassimo/vite_ruby ↩
-
“Hot Module Replacement API,” Webpack, accessed March 21, 2026, https://webpack.js.org/api/hot-module-replacement/ ↩
-
“Comparison with webpacker (shakapacker),” jsbundling-rails GitHub, accessed March 21, 2026, https://github.com/rails/jsbundling-rails/blob/main/docs/comparison_with_webpacker.md.\n[^7]: “The Ruby on Rails Doctrine,” Ruby on Rails, accessed March 21, 2026, https://rubyonrails.org/doctrine ↩
-
Shane Larson, “Vite for Development: Fast Builds and HMR,” Grizzly Peak Software, February 14, 2026, https://www.grizzlypeaksoftware.com/library/vite-for-development-fast-builds-and-hmr-804sgphu ↩ ↩2
-
“Getting Started With Vite on Rails,” FastRuby.io, January 20, 2026, https://www.fastruby.io/blog/getting-started-with-vite-on-rails.html.\n[^13]: “Webpacker Retired,” Reddit r/rails, January 19, 2022, https://www.reddit.com/r/rails/comments/s7oqfa/webpacker_retired/ ↩ ↩2
-
“Rails Integration,” Vite Ruby, accessed March 21, 2026, https://vite-ruby.netlify.app/guide/rails.html ↩ ↩2 ↩3 ↩4
-
“Deployment,” Vite Ruby, accessed March 21, 2026, https://vite-ruby.netlify.app/guide/deployment ↩
-
GitHub issue discussion, accessed March 21, 2026, https://github.com/ElMassimo/vite_ruby/issues/491 ↩
You May Also Like
Improving Frontend Security with Strict Content Security Policies in Rails 8
Learn how to mitigate Cross-Site Scripting (XSS) and meet compliance requirements using nonce-based Strict Content Security Policies (CSP) in Rails 8.
Insecure Direct Object References (IDOR) in Rails: Proper Authorization Checks
A guide to understanding and preventing Insecure Direct Object References (IDOR) in Ruby on Rails applications with proper authorization checks.
Lazy-Loading Frontend Components in a Rails Inertia.js Architecture
How to implement code splitting and lazy loading in a Ruby on Rails application using Inertia.js to improve frontend performance.