Rails PWA: Put CSS and JS in Browser cache for offline mode

by Kim Laplume

Summary

Put your CSS and JS assets in a Browser cache, as a first step into offline functionality

Lesson

In order to even start with a reasonable Progressive Web App (PWA) story, some of the assets need to be put in a cache up front by the browser, so it can serve those assets instead of going to the network to fetch them. The following code illustrates how to achieve the CSS and JS (and one example HTML page) with the new Rails 8 PWA routes and views and Propshaft as the asset pipeline.

A new Rails 8 project comes with those lines commented out in your routes.rb file, uncomment them

# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker

Next edit the file app/views/pwa/service-worker.js.erb (if there is a file without the erb-extension, rename it) and put the following

var version = 'v1::';

function onInstall(event) {
  console.log('[Serviceworker]', "Installing!", event);
  event.waitUntil(
    caches.open(version + 'offline').then(function prefill(cache) {
      return cache.addAll([
        '/offline',
        <%== Rails.application.assets.load_path.manifest.values.select { |e| e.ends_with? ".js" }.map { |s| "'/assets/#{s}'"}.join(',') %>,
        <%== Rails.application.assets.load_path.manifest.values.select { |e| e.ends_with? ".css" }.map { |s| "'/assets/#{s}'"}.join(',') %>
      ]);
    })
  );
}
self.addEventListener('install', onInstall);

To have this service worker properly installed, a service worker companion is still needed. Add a file under app/javascript/service-worker-companion,js with the following content

if (navigator.serviceWorker) {
  navigator.serviceWorker.register("/service-worker.js", { scope: "/" })
    .then(() => navigator.serviceWorker.ready)
    .then((registration) => {
      // if ("SyncManager" in window) {
      //   registration.sync.register("sync-forms");
      // } else {
      //   window.alert("This browser does not support background sync.")
      // }
    }).then(() => console.log("[Companion]", "Service worker registered!"));
}

Finally, this file needs to be pinned in your config/importmap.rb file

# ...
pin "service-worker-companion", to: "service-worker-companion.js"
# ...

And imported in your main JS entry point app/javascript/application.js

// ,,,
import "service-worker-companion"
// ...

When opening the page, you should see in the console the service worker installed
(Output: [Companion] Service worker registered! and [Serviceworker] Installing!) and most importantly in your Application Tab in the Browser Dev Tools (or named similarly, depending on what you use) a cache named v1::offline that holds your CSS and JS files.

In order to make use of the cached assets in the browser, it is now up to you to write a Rails controller and view for the path /offline that should be cached as well on service worker installation and that you can visit without being connected to the Internet.