Offline-First

Summarized using AI

Progressive Web Apps for Rails Developers

Emmanuel Hayford • September 26, 2024 • Toronto, Canada

In the video titled "Progressive Web Apps for Rails Developers," Emmanuel Hayford discusses the advantages and development of Progressive Web Apps (PWAs) within the Ruby on Rails framework, particularly with the upcoming Rails 8. Key points covered include:

  • Introduction to PWAs: Hayford emphasizes the significance of PWAs, highlighting their capability to maintain app availability without an internet connection, use push notifications for user engagement, and implement caching systems for efficient asset delivery.

  • Benefits of PWAs Over Native Apps: He outlines the drawbacks of traditional app development, especially concerning app store review processes, and illustrates how PWAs eliminate these issues, providing cost-effective solutions for app development.

  • Successful PWA Examples: Companies like Tinder and Pinterest showcase substantial improvements in performance and engagement, with Tinder reducing load times significantly and Pinterest witnessing a 60% increase in user interaction following their PWA implementation.

  • Building a PWA in Rails 8: Hayford details the essentials for constructing a PWA, which include a web application manifest file, a service worker, and a secure context (HTTPS). Rails 8 simplifies this process by automatically generating necessary files when a new app is created.

    • Manifest and Service Worker: He explains the purpose of the manifest file in making the app installable and the role of the service worker as an intermediary that manages caching and background tasks.
  • Cache API vs. HTTP Cache: The advantages of using the Cache API are presented, particularly its programmatic control over resources, which is critical for providing reliable offline experiences.

  • Offline Functionality: A step-by-step approach is provided to implement offline functionality, including caching strategies, handling fetch events, and managing fallbacks for offline content.

  • Background Sync and IndexedDB: Hayford introduces the Background Sync API, allowing apps to send data to servers when users regain connectivity, and discusses using IndexedDB for storing user data while offline.

  • Push Notifications: Although not fully covered in the talk, he outlines the basic workflow required to implement push notifications in PWAs and mentions the upcoming Action Notifier framework in Rails 8.1 that simplifies notifications.

In conclusion, Hayford presents PWAs as a powerful tool for Rails developers, poised to simplify app construction while enhancing user experience. The talk serves as an introduction to building functional PWAs using the Ruby on Rails framework, reinforcing the benefits of adopting this approach in modern web development.

Progressive Web Apps for Rails Developers
Emmanuel Hayford • September 26, 2024 • Toronto, Canada

Rails 8 will simplify PWA development by generating essential PWA scaffolding by default. In his #RailsWorld talk, Emmanuel Hayford covers PWA basics, the service worker lifecycle, offline strategies via background sync, and the CacheStorage API for cross-device performance.

Thank you Shopify for sponsoring the editing and post-production of these videos. Check out insights from the Engineering team at: https://shopify.engineering/

Stay tuned: all 2024 Rails World videos will be subtitled in Japanese and Brazilian Portuguese soon thanks to our sponsor Happy Scribe, a transcription service built on Rails. https://www.happyscribe.com/

Rails World 2024

00:00:09 Hello, everyone!
00:00:11 How many of you are excited about Rails 8? Show of hands! That's right! My name is Emmanuel Hayford, and I host the Rails Change Log podcast. I encourage you to check it out, and I'm on Twitter as @saw23, so feel free to say hi! Today, I'm excited to talk to you about Progressive Web Apps (PWAs) and what you can do with them.
00:00:24 So, why should you consider PWAs? First of all, PWAs provide app availability even when a user has a flaky connection or no internet connection at all. Additionally, they allow access to push notifications to re-engage your users if you need to notify them of something. Lastly, PWAs come with a caching system, enabling you to serve assets from the cache instead of the server.
00:00:42 You know what's going on with Apple these days, right? If you have an iOS app or an Android app, well, Android is not so bad, but Apple is notorious for the app review process. When you submit an app update, you often have to wait for a review, and sometimes they decline it for the most trivial reasons. With PWAs, you don't have to worry about any of that. You also save money on hiring a dedicated team to build native applications.
00:00:58 Here are some companies reaping the benefits of PWAs: Tinder reported cutting load times from 12 seconds to just 5 seconds with their new PWA. Pinterest built their mobile site as a PWA, leading to a 60% increase in user engagement. That's a significant improvement!
00:01:05 Snapchat also uses a PWA and has stated that it has doubled their daily active users. Users on older devices spend 11% more time interacting with a PWA than with native apps. They also click to install PWAs 40% more often than they download native apps. Though, I have to say, I might not entirely agree with that statement. Perhaps they click to install PWAs because it sounds cooler, right?
00:01:23 So, PWAs are a big deal! In fact, I know one Ruby on Rails developer who ditched some of their Apple products because of PWAs. They said, 'I don't want to use Apple stuff anymore! The final straw was when Apple said they were going to pull PWAs out of Europe.'
00:01:37 Yes, DHH has said this too. PWAs are huge for me as well. Today, we will talk step by step about the process of building a PWA.
00:01:44 First, what do you need to build a PWA? You just need three items: a web application manifest file, a service worker, and a secure context, which can be anything running through HTTPS or your local host. In Rails 8, if you generate a new app, you will see two essential files: a PWA folder under app/views, containing a manifest.json file, and your service worker file.
00:02:08 This is how a manifest file looks. A manifest file is just a JSON text that provides information about your web app. It is the manifest file that makes your web app installable, giving it that native feel. A service worker is a JavaScript file with various events and functions to control your web app. Once a PWA is installed, you can tell it apart by looking at its icon.
00:02:30 Here's a screenshot of my desktop with some applications. How many of you can tell which of these apps are PWAs? Show of hands! You cannot? That's right! There are two apps that are PWAs: the first one, which you obviously know, is the real-world one that everyone recognizes, and the other is Basecamp.
00:02:54 A service worker acts like a background helper for your web app. It serves as a middle layer between your app and the internet. In a way, you have the main browser thread, and then you have the worker thread. The worker thread runs in a separate thread and has its own workspace. Once the browser registers it, it can control requests between the server or your cache storage and the browser thread.
00:03:14 It handles things like caching, notifications, and background synchronization. So, how does Rails actually set up PWAs for your applications? First, we need to look at the manifest file, which is needed in the head tags. Rails 8 has this line commented out, so you need to uncomment it.
00:03:28 I must mention that you can still use PWA features without a manifest file, but your app won't be installable. You'll also need to uncomment the routes that serve the service worker and the manifest files. At this point, we have the basic files hooked up, but there is one critical step to take.
00:03:45 We need to write JavaScript to tell the browser to actually register our service worker file. Inside your application.js file, we check if the browser supports service workers. If it does, we use the register method of the service worker container interface to register our service worker.js file, which is served by Rails. The register method returns a promise that we can attach then calls to grab the service worker registration object.
00:04:04 If for some reason the registration fails, we log an error; otherwise, we log that the user's browser does not support service workers. That's it! Our Rails 8 app is now a PWA. Now let's take a look at how these components look from the user's perspective.
00:04:26 First, a user navigates to a PWA-enabled application. The browser downloads, parses, and executes the service worker. The install event is activated once the service worker is executed. After successfully installing, the service worker is ready to intercept requests and handle events.
00:04:48 Now, there are two technologies that I want to talk about briefly. They are somewhat at odds with each other. There is the Cache API, which gives PWAs their power, and then there is the general browser HTTP cache. The Cache API provides programmatic control over what gets cached, how it's cached, and when it's updated or deleted.
00:05:07 It also provides a JavaScript interface to interact with cached responses directly within the service worker. Caches are scoped to the origin, ensuring specific isolation between different PWAs. There are many differences; you can't really control the browser HTTP cache as much as you can control the Cache API. PWAs rely heavily on caching to provide fast, reliable experiences, especially when offline.
00:05:28 So, what do we need to have our application be a PWA? First, we hook into the install event, then call event.waitUntil. This method tells the event dispatcher that there's some ongoing work, letting the browser know it shouldn't kill the service worker. Essentially, it extends the install event's lifespan.
00:05:51 Next, we create or open a cache called V1. This cache is similar to the find or create-by method in Ruby on Rails. If the cache exists, it will be used; if not, it will be created. After that, we grab the cache object and save all the assets we want to keep offline in the V1 cache.
00:06:11 In this example, I'm assuming that HTML and application.js are everything my application requires. This is just an illustration, but I'm also providing a fallback called offline.html. Next, we add an event listener for the fetch event. The fetch event in the service worker is triggered when the main app thread makes a network request.
00:06:34 Remember, your application operates on its own separate thread, while the service worker also has its own separate thread. We return a custom response using the fetch event's respondWith method. This enables the service worker to intercept network requests and respond with custom responses.
00:06:54 We check if a match for the request is found in the cache. If a response is found, we serve it; if not, we fetch the request from the network. We open the V1 cache we created earlier and add a clone of the request to it using the cache.put method. We then return the response from the network.
00:07:13 If, for some reason, the network fails, we return a fallback response. Our PWA now works offline, serving static files. That's basically it!
00:07:31 Now, there's a joke about PWAs. Did anyone hear it? No one? That's fine! We can cache assets and save them when a user is offline. Let's step things up a notch.
00:07:42 What if a user wants to send data to the server? Right now, we only have assets downloaded locally. That's when the Background Sync API and IndexedDB come into play. The app works offline, but that's still not enough. We need to send user data to the server. Imagine a CRM where a user drafts a blog post offline, and then the post gets published once the user comes back online.
00:08:01 For this purpose, we'll use IDB Keyval. IDB Keyval's API resembles how you would use local storage to assign items to keys. Here's an example: first, we set a value; we import the library, then we assign a value to a key and retrieve it using the get method. That's it! Of course, there are other methods as well, but for our purposes, these are sufficient.
00:08:23 Assuming this is a form in a blogging CRM, we're only interested in the title and content of a blog post. We ID the form with 'blog_form', and take note of the ID, as we can grab it later with JavaScript.
00:08:43 Then we move to the file that registers our service worker, which is the application.js file. This time, we check for something different: whether the browser supports the SyncManager. The SyncManager provides an interface for registering and listing sync registrations for the service worker to catch sync events.
00:09:01 To catch sync events, you must register a sync. In other words, you need to request from the service worker that you have new data to sync to the server. If the browser supports both service workers and sync managers, we register our service worker as usual.
00:09:21 We then create two functions: one for saving the blog post to IndexedDB while the user is offline, and another for sending the blog post to the Rails backend when the user is online. Remember the set method from IDB Keyval? We set a key and assign the blog content as the value.
00:09:40 Then, we register a sync event using the SyncManager's register method. The argument you pass to the SyncManager's register method is referred to as a tag. The tag should be unique for separate tasks, allowing us to identify them and respond when needed.
00:10:02 Next, we create a function to send the blog content using the fetch API if the user is online. If the user is offline, we save the content of the blog post in the browser's IndexedDB.
00:10:21 But there are two more steps we need to complete. The next step is to grab our form, 'blog_form' (which is in snake case), listen for a submit event, and then grab the content and title of the blog post submitted by the user.
00:10:37 When this happens, we can send the data. Here's a refresher on how the 'send blog' function looks if you've forgotten.
00:10:52 The last remaining step is to instruct our service worker on what to do when a sync request comes in. First, we import the IDB Keyval library into our service worker since we need to use its get method to retrieve what we've saved from the frontend. Remember, the service worker and the app in the browser are running in separate threads, and they cannot access each other's workspaces. In fact, the service worker doesn't even know what the DOM is.
00:11:09 This time, we're looking to respond to the 'sync-blog' event that we created earlier. This tag is used to identify our sync event. When our sync event is triggered, we get the content we stored earlier in IndexedDB. This content would be the blog post sent by the user, which we'll send to our Rails backend once the user regains internet connection.
00:11:29 But there’s one last thing. After sending the blog post, we need to delete it from the browser's database. You don’t want to be sending the same post repeatedly each time the sync event is triggered. In the service worker, we can catch and respond to errors at this stage, but almost forgot to explain how to deal with all of this on the server.
00:11:45 On your blogs controller, you will have something like this: first, you have a create event, assuming you've already set up the resource in the routes.rb file. If the blog is saved, you return a JSON message indicating it has been saved; otherwise, you return a message stating it has failed.
00:12:06 You remember how I used IDB Keyval to set and retrieve values from IndexedDB? I opted for IDB Keyval because the API for IndexedDB can be quite convoluted. To use IndexedDB itself, you must first create a database within the user's browser.
00:12:21 The code you see here exemplifies how to create a database in IndexedDB. Notice the callbacks; there's an 'onupgradeneeded' event that runs whenever the schema in the database changes, and a callback for when a query is successful. You'll need to follow this structure for almost everything.
00:12:41 This is an example code snippet for creating a new database. You would follow similar steps for retrieving an item or saving something to the database. That's why, in some cases, it's better to use IDB Keyval unless you have a compelling reason to work directly with IndexedDB.
00:13:04 We didn't cover push notifications when I wrote this talk. For some reason, I thought I'd cover all the steps involved, but here's how push notifications would work: First, you register a service worker (which we've already discussed), and then respond to a user gesture, which is when you ask for permissions.
00:13:23 Then, you ask the user for a push subscription and send the subscription data to the server. After that, you store the subscription data in your database, and when something occurs that requires a notification, you send a push message to the relevant subscription.
00:13:47 This process can be quite complicated. For that reason, in Rails 8.1, the Rails core team and Basecamp are providing an Action Notifier framework, which simplifies the entire notification process.
00:14:05 So, we've covered IndexedDB, the Cache API, web notifications, caching assets locally, and that's what PWAs are all about. When Rails 8 is released, or when Rails 8.1 comes out, you'll be able to build a fully functional PWA application.
00:14:22 This has been an introduction to PWAs in Ruby on Rails. My name is Emmanuel Hayford. Thank you!
Explore all talks recorded at Rails World 2024
+31