Welcome to Day 5 of #30DaysOfPWA! New to the series? Three things you can do to catch up:
- Read the kickoff post below for context.
- Watch the GitHub Repository for updates.
- Bookmark #30DaysOfPWA Blog for resources.
Welcome to #30DaysOfPWA
Nitya Narasimhan, Ph.D for Microsoft Azure ・ Feb 14 '22
This is a shortened version of this canonical post for the #30DaysOfPWA.
What We'll Cover Today
- What does offline mean for PWA?
- How does a PWA know about network changes?
- What storage options can service workers access?
- What caching strategies can PWAs employ?
- Exercise: Explore offline experiences in sample PWAs
Let's Recap
In our last post we learned that Service Workers are a type of web worker that acts as a network proxy (intercepting fetch requests), manages cache storage intelligently (for offline operation) and handles async events like push notifications (to support re-engageable behaviors). We learned about Service Worker registration, and lifecycle events that lead to it being activated.
The service worker is ready to go to work- now what? Today we'll look at how the service worker handles the functional events, with emphasis on the fetch and cache related APIs that drive network-independent offline-ready experiences.
What does "Offline" mean?
A key difference between native (installed) and web (in-browser) experiences has traditionally been about network dependency. Regular websites won't function reliably if you are off network (e.g., flight mode) or have poor network connectivity (in transit or low-bandwidth regions).
Service workers can help make PWAs network independent by making sure they deliver a usable experience even under flaky or absent network conditions. Some recommended practices here include:
- Providing a custom offline page. Rather than showing the browser's default offline page, this can show something meaningful that keeps the user engaged in the app until the network connection is restored. For instance, pre-cache a custom offline page that reflects the app's branding; show it when user is offline and navigates to an un-cached page or route, giving them confidence that the app is in charge and knows about current network status.
- Proactively cache assets or responses with longevity. There may be content in your app that remains unchanged for long intervals - e.g., banner images, authentication state, media for playback etc. Proactively cache and use them even if online for improved performance and native-like behaviors.
Now, when service workers intercept a "fetch" request, they can evaluate different strategies for returning a response. For example: an "offline first" strategy might aggressively prioritize cache over network when returning responses. While this may bring performance benefits (reduced network delays) it also requires managing priorities of cached elements for impact.
In the next couple of sections, let's look at the code that helps us implement these strategies in our service worker!
But first, take a minute to inspect your chosen Sample PWA and look at its service worker implementation. Keep that open in a tab so you can explore it in the context of this discussion. I'm using DevTools Tips and I've made a copy of its sw.js
file in this gist for my reference.
Understanding Storage Options
Making resources available offline requires taking advantage of on-device storage. Given their async nature, service workers (web workers) have access to two options:
- CacheStorage - an API to a store of named Cache objects that can be accessed by both service workers and an app’s main JavaScript thread. Caches store request/response pairs for network resources. Caches need to be managed explicitly - for updates and deletes - with quotas set per origin. A service worker can have multiple named Cache objects if needed.
- IndexedDB - an API to store large amounts of structured data including files and blobs. It's an object-oriented transactional database that uses key-value pairs and is ideal for storing individual assets (vs. response pages in cache). You can read more about IndexedDB in week 4 Platforms & Practices.
Note that Web Storage options (localStorage and sessionStorage) are synchronous and can't be used within web workers - but you could use them from the main thread with potential performance hits. Learn more about that here. Want to get a better sense of storage options? Just inspect the Application Panel of the PWA using DevTools and debug interactively.
Take a look at the Microsoft Edge docs for the Service Workers, Cache Storage and IndexedDB panels to learn how to explore them in real contexts.
Cache Storage & Strategies
Caches need to be managed explicitly - creation, modification and deletion of resources must be done by the service worker with intent. Cache usage is dependent on the strategy used for handling fetch requests. The Offline Cookbook is a great resource that provides insights into all three questions:
- what to cache (real-time vs. long-living resource types)
- when to cache it (on install, activate, fetch events)
- how to handle fetch requests (cache first, network first, a combination)
Let's look at a couple of strategies, with code from the sample PWA.
1. Cache on install to improve performance
Here's the relevant snippet of code from the DevTools Tips sw.js gist, annotated with my comments for clarity
// named cache in Cache Storage
const CACHE_NAME = 'devtools-tips-v3';
// list of requests whose responses will be pre-cached at install
const INITIAL_CACHED_RESOURCES = [
'/',
'/offline/',
'/all/',
'/browser/edge/',
'/browser/safari/',
'/browser/firefox/',
'/browser/chrome/',
'/assets/style.css',
'/assets/filter-tip-list.js',
'/assets/share.js',
'/assets/logo.png',
'https://unpkg.com/prismjs@1.20.0/themes/prism-okaidia.css',
'/assets/localforage-1.10.0.min.js'
];
// install event handler (note async operation)
// opens named cache, pre-caches identified resources above
self.addEventListener('install', event => {
event.waitUntil((async () => {
const cache = await caches.open(CACHE_NAME);
cache.addAll(INITIAL_CACHED_RESOURCES);
})());
});
This gets key request-response pairs pre-cached for offline readiness. But how are these retrieved - and when?
2. Cache-first on (fetch) retrieval
Now, when a fetch event is received, the service worker can enforce its preferred policy - here a cache first strategy means that the service worker looks for a match in the cache and only goes to the network on a miss. Note that you can pre-filter on request parameters to refine the policy - e.g. go to cache only if resource type is an HTML page etc.
Here's the relevant snippet from our sample PWA.
// We have a cache-first strategy,
// where we look for resources in the cache first
// and only on the network if this fails.
self.addEventListener('fetch', event => {
event.respondWith((async () => {
const cache = await caches.open(CACHE_NAME);
// Try the cache first.
const cachedResponse = await cache.match(event.request);
if (cachedResponse !== undefined) {
// Cache hit, let's send the cached resource.
return cachedResponse;
} else {
// Nothing in cache, let's go to the network.
// ...... truncated ....
}
}
}
Detecting Network Changes
The above examples showcased scenarios where the caching policy was independent of network status (i.e., always checks cache first). But what if you wanted to condition your strategy on the current status of the network?
The navigator.onLine
property returns a boolean (true/false) value reflecting the online status of the browser. Different browsers might implement this differently - so you may want to understand nuances to avoid false positives and negatives. The property should send update events if that status changes - and you can listen for those events at the window
level.
window.addEventListener("online", function(){
console.log("You are online!");
});
window.addEventListener("offline", function(){
console.log("Oh no, you lost your network connection.");
});
So how can this event listener notify the Service Worker of that network change? Service Workers and their clients can communicate using the postMessage API. On the client side, this involves sending a message with relevant data:
navigator.serviceWorker.controller.postMessage({
type: `IS_OFFLINE`
// add more properties if needed
});
On the service worker side, it listens for "message" events and unpacks the data to take further action:
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'IS_OFFLINE') {
// take relevant actions
}
});
You can also use the navigator.connection property to access information about network quality. This includes:
- navigator.connection.downlink to get effective bandwidth estimate (if available)
- navigator.connection.saveData if user activated the "save data" option in their browser
A Visual Summary
Service Workers can also do other things - like handle push notifications or fetch data in the background and use that to update app or cache for efficiency. Look for content in week 4 (Platforms and Practices) that might be relevant in context.
That was quite a lot, right? Hopefully this visual summary (of everything we talked about on Service Workers, Storage Options and Caching Strategies) will help you review and recall key elements.
Learning Resources
Working with service workers can be challenging. There are a number of resources worth exploring on your own, to improve your understanding and simplify your developer experience.
- Modern Browser Support | Sites like caniuse.com and is Service Worker ready? provide easy-to-read dashboards showing the degree of support for specific Service Worker API and features across browsers.
- Service Worker API | This MDN resource has good coverage of Service Worker interfaces, use cases and related APIs.
- Storage Options | Learn more about Cache Storage API, Cache API and IndexedDB API - to understand what types of data each supports, and how they can be used by service workers.
- Workbox | These Google-developed libraries power production-ready service workers and outline common recipes for caching strategies you can use in your PWA.
Look out for content in the following weeks that may explore some of these topics in more detail.
Exercise: Your Turn!!
You know the drill! Pick a Sample PWA and inspect it in browser DevTools.
- Open Service Worker panel, and view the implementation file.
- Look for the install event handler - correlate it to cache contents. Was anything pre-cached on startup?
- Look for the fetch event handler - what kind of caching strategy is the PWA enforcing?
- Toggle "Offline" mode in DevTools Service Worker panel. Navigate back to different pages of the app. What do you see?
- Do you get a custom offline page for non-cached assets?
- Do you get valid pages when you navigate to pre-cached routes?
- Review the rest of the service worker implementation - try to understand what events it is handling, and how those impact caching strategies and offline behaviors.
Want to read more content from Microsoft technologists? Don't forget to follow Azure right here on dev.to: