The vite_rails gem brings Vite’s excellent frontend developer experience to Rails apps. It’s my preferred frontend tooling for most Rails projects. One of its headline features is hot reloading of CSS: touch a CSS or SCSS file, and the changes are rendered instantly in the browser.
Unless the browser is Safari, unfortunately. There is a known bug where Safari renders an older, cached version of an app’s CSS, rather than the updated one provided by Vite. Vite correctly sends its CSS with the
cache-control: no-cache header, but Safari ignores the header and caches it anyway.
As of Safari 16.4, there is no easy way to disable this unwanted caching behavior; even the “Disable Caches” option in the dev tools network tab has no effect.
vite_stylesheet_tag. This works, but re-architecting an app’s frontend entry points seems like a drastic step. Especially when the problem occurs only in development mode, and only in Safari.
Arguably a lighter touch solution, requiring no frontend CSS/JS code changes, is to add a cache-buster to the CSS URL. This technique has been used since the earliest days of the web to defeat caches. A set of small monkey-patches to the Rails stylesheet helper and vite_rails does the trick:
Now, when a stylesheet is referenced like this:
<%= vite_stylesheet_tag("application.scss") %>
The generated link tag will include a random cache-buster on every page load in development:
<link rel="stylesheet" href="/vite-dev/entrypoints/application.scss?cb=f55d7ceb" />
This guarantees that Safari will request the CSS rather than using a stale version from cache. The result is that we get Vite’s awesome hot-reloading experience in Safari, and we didn’t have to reorganize our frontend code!
Monkey patch solutions like this do have risks, but our production code is not dependent on the patch, which limits the scope of the impact. If a future version of Rails or Vite causes the patch to break, we can easily roll back the patch by deleting a single file.