Taking Rails Offline

Make your Ruby on Rails apps resilient to unreliable networks & also improve app performance.

What are we going to do?

  • We're going to go through scenarios where making some data available offline is advantageous
  • I'll run you through the approach
  • I'll show you the libraries to help you get going quickly.
  • We'll go through the gotchas & the wins

Why would you want to do this?

Chuck up an emoji if the scenarios I'm pitching sound familiar!

How are we going to do this?!

We're going to use Service Workers

How are we going to do this?!

How are we going to do this?!

How are we going to do this?!

How are we going to do this?!

How are we going to do this?!

How are we going to do this?!

In a Traditional Request

With a Service Worker

With a Service Worker

Adding a Service Worker to Rails

Adding a Service Worker to Rails

// app/assets/javascripts/application.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker      .register('/service-worker.js', { scope: "/" })  });
}

Adding a Service Worker to Rails

// public/service-worker.js

self.addEventListener('install', function(event) {
  // We've been added. Pre-cache some stuff
});

// A request is being made
// Load a file from the cache, or request it from the network
self.addEventListener('fetch', function(event) {
  // return fetch(event.request);
  console.log(event);
  debugger;
});

Adding a Service Worker to Rails

Adding a Service Worker to Rails

Is there a Gem for this?

Yes! I found two awesome ones!

serviceworker-rails & webpacker-pwa

Which gem is the best?

serviceworker-rails webpacker-pwa
Works via the Asset Pipeline Works with Webpacker
One command to install More complex to setup
You set up an array of files to cache Can set more complex rules for what gets cached
Does not support Webpacker Modern JS (You can use Google Workbox)
Default is just an offline fallback Does not ship with a default file

serviceworker-rails

https://github.com/rossta/serviceworker-rails

$ bundle add serviceworker-rails
$ rails g serviceworker:install

serviceworker-rails

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   app/assets/javascripts/application.js
	new file:   app/assets/javascripts/manifest.json.erb
	new file:   app/assets/javascripts/serviceworker-companion.js
	new file:   app/assets/javascripts/serviceworker.js.erb	modified:   app/views/layouts/application.html.erb
	modified:   config/initializers/assets.rb
	new file:   config/initializers/serviceworker.rb
	new file:   public/offline.html

serviceworker-rails

// app/assets/javascripts/serviceworker.js.erb
function onInstall(event) {
  console.log('[Serviceworker]', "Installing!", event);
  event.waitUntil(
    caches.open(CACHE_NAME).then(function prefill(cache) {
      return cache.addAll([
        '<%= asset_path "application.css" %>',        '/offline.html',      ]);
    })
  );
}

serviceworker-rails

serviceworker-rails

// app/assets/javascripts/serviceworker.js.erb
function onFetch(event) {
  event.respondWith(fetch(event.request).catch(function() {
    return caches.match(event.request).then(function(response) {
      if (response) { return response; }

      // Fallback to offline.html
      if (event.request.mode === 'navigate') {
        return caches.match('/offline.html');
      }
    })
  }));
}

serviceworker-rails

webpacker-pwa

https://github.com/coorasse/webpacker-pwa

$ bundle add webpacker-pwa --group=development
$ yarn add webpacker-pwa

Then you'll also need to edit config/webpack/environment.js & config/webpacker.yml.

webpacker-pwa

// app/javascript/service_workers/service-worker.js
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst } from 'workbox-strategies';

registerRoute(
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    cacheName: 'pages',
    plugins: [],
  }),
);

webpacker-pwa

webpacker-pwa

webpacker-pwa

webpacker-pwa

The Gotchas

  • URL of service worker must stay the same, e.g. /service-worker.js
  • Cache size is pretty varied between devices, ~25MB ( https://stackoverflow.com/a/35696506/445724 ) is the maximum
  • I've had one running on my local dev & a page not updating.
  • It is tampering with requests, so can go wrong
  • There isn't a perfect gem or configuration for Rails apps.

Not suitable for all use apps

Way too big a footgun for a site like IKEA in my opinion. People tend to visit less frequently and with less patterns than to, for example, a social network

Robin Whittleton, engineering manager for IKEA (His opinion, not IKEAs)

The Wins

  • Cache some assets (like JavaScript & CSS) ahead of time like Twitter
  • Offer a few key pages offline (e.g. top news articles)
  • You can fetch slow content ahead of time
  • They can can be used for background syncing
  • Useful for DDoS mitigation

Thank you

Twitter: @MikeRogers0

Blog: mikerogers.io

This started off as a me just messing around making my Rails apps "Progress Web App' I ended up quite liking the results & wanted to share them! Just an FYI! I'm going to mention a few resources in the talk, I'm going to put links to everything at the end.

This is my plan for what we'll talk about! We have about 20 minutes! Fingers crossed!

So where does _this_ come from! I have a few use cases, if you've experienced this throw up some emojis: - You go onto a train, maybe it goes underground & no network is unavailable. Then maybe you'd just want to check the news or the next train times. - You're at home & someone on your local network eats all the bandwidth, so pages get slow. - Website you visited quite recently just goes down These are all problems we can mitigate against!

To do that we use bit of browser technology called a Service Worker. It has pretty good browser support, who has heard of them? Chuck up your emojis if you heard of them! They landed in browsers around 2016, but they didn't make it to iOS until late 2018.

You probably have not noticed them quietly running on your machine. They pretty much are just JavaScript file we can run in the background while a user is looking at our webpage. You can use it for a bunch of stuff, but mainly it's caching files & tampering with requests to avoid touching the network.

Lots of websites quietly use them to download CSS & JavaScript assets. If you open the Storage tab in your dev tools, you can see what's being saved.

We're downloading a lot of files here, but they're all take 0ms...they're all coming from the service worker also.

Because all the files are cached offline, you get this happy side effect.

You can visit the debugging page in your browser & you can see them all working (along with all the ones you've collected). When I first opened this page I found like 30 of them there. If you smash that inspect button, you can also see their code.

Which is quite interesting to look at! How are we feeling about Service Workers? We all understand it's javascript file we can use to tamper with requests & cache stuff? Throw up some emojis for me!

So pretty much in a traditional request, the app will always touch the network for things like assets and new data.

When we add the service worker, we can say "Actually, we've have this file - Don't touch the network".

What this allows is if the network is slow or not around, we can use that file we cached.

How are we doing for time? How do we add this magic to rails?

We use a little bit of JavaScript to our application.js to say "If you can use run service workers, go run ours" Then the browser will go fetch that file.

Normally that file will look a bit like this!

If you're curious about writing one of these yourself have a look at: servicewore.rs - It's a cookbook site by Mozilla. It coverages how you'd implement strategies, e.g: 1. Downloading ahead of time 2. Asking the cache first for the file

They have a bunch of samples you can mess around with. I did start writing a JavaScript file which should nicely for Rails for people, but then I asked the question:

Turns out awesome people have written stuff for us to use :) Let me quickly summarise them!

serviceworker-rails - It's the easier choice, it ships a working JS file & you will tweak it based on your needs. webpacker-pwa - It's more complicated, but you can use a library called WorkBox to configure your caching rules.

serviceworker-rails - It's very easy to install, but limited.

These are the files it adds, the only important one is serviceworker.js.erb

The key thing to note in that file is you list the files you'd like available offline

Then it'll cache everything

The key thing to note in that file is you list the files you'd like available offline

The end result is if the users networks goes down, or the users wifi goes out & the file isn't cached they see this page. It's a nice starting point, but not amazing.

Setting this one up requires a bit more setup.

But you'll be able to use Google Workbox, which is like Swiss army knife for writing Service Worker JS & it not becoming a giant complex mess.

Which is JavaScript library which has extracted lots of the complexity of writing Service Workers into something cleaner.

And they have a pretty nifty cookbook, which should mean things are pretty standard.

But the end result for this gem was: I had quite good performance for loading CSS/JS assets

Also without me having to explicitly predefine what I wanted offline, it had built cache of pages which were ok to display offline.

Then once I turned my network off, my app still loaded.

- The URL having to stay the same is big hurdle. If you change the URL, you'll end up with multiple service workers running. - I couldn't find any good documentation about how much you can store in cache, someone on StackOverflow said 25MB - A good example for devices is iOS. I found I needed to bookmark my app on my homescreen for it to work, but on my browser is worked nicely. - Messing with requests can backfire. I had a friend misconfigure his, and it broke form submissions in production...they didn't notice for a while. - Not every app needs one...

I got to talk with someone who works on the IKEA website, and they had an interesting insight. If a user is only going to touch a few core pages a few times a year, it's not worth the effort.

- Twitter ships like 5mb of JavaScript & fonts, which is massive. But the second time you visit it, it's pretty quick to load. - It's pretty cool to cache home pages - Say you've got client who is complaining a single page is slow, you could pre-cache it! - A social network in early January was able to make their feed read-only when their main site when down.

DHH says I'm cool. He was talking about Hotwire & how Service Workers play nicely with HTML over the wire. So maybe something is coming to Rails...

Breath & tell them your name! Does anyone want to see a demo?