Make your PWA work offline with a Service-Worker

Louis Japheth Kouassi
9 min readAug 3, 2023
Make your PWA work offline

At the end of this article, you will be able to :

  • know what it means to be online ;
  • easily define a service-worker ;
  • know the storage options ;
  • know the storage strategies ;

So, let’s start 😉!

What does being offline mean?

The expression “to be offline” means that one is not connected to the Internet. This could be due to a number of factors, such as a power outage, an interruption in internet service, or a deliberate choice to go offline.

In the context of technology, being offline means that the device or network is not connected to the Internet. This means that the device or network cannot access information or services that are stored or available online.

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 platform-specific behaviors.

What is a service worker?

What is a service worker?
What is a service worker?

A service worker is a background script that adds offline functionality to a web application. A service worker can be used to :

  • serve resources, such as images, scripts, and stylesheets, even when the web application isn’t connected to the internet ;
  • handle push notifications and process network requests.

Here are some examples of uses for service workers:

  • Resource caching to improve performance
  • Management of notifications to send messages to the user
  • Managing network connections to improve reliability
  • Geolocation management to deliver personalized experiences
  • Control of file downloads
  • Manage user input to provide smoother experiences

Service workers are a powerful tool that can be used to improve the performance, functionality, and user experience of Progressive Web Applications. If you are developing a progressive web application, you should consider using service workers.

What are the 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:

✔️Cache : The Cache interface provides a persistent storage mechanism for Request / Response object pairs that are cached in long lived memory. How long a Cache object lives is browser dependent, but a single origin's scripts can typically rely on the presence of a previously populated Cache object. Note that the Cache interface is exposed to windowed scopes as well as workers. You don't have to use it in conjunction with service workers, even though it is defined in the service worker spec.

const CACHE_VERSION = 1;
const CURRENT_CACHES = {
font: `font-cache-v${CACHE_VERSION}`,
};

self.addEventListener("activate", (event) => {
// Delete all caches that aren't named in CURRENT_CACHES.
// While there is only one cache in this example, the same logic
// will handle the case where there are multiple versioned caches.
const expectedCacheNamesSet = new Set(Object.values(CURRENT_CACHES));
event.waitUntil(
caches.keys().then((cacheNames) =>
Promise.all(
cacheNames.map((cacheName) => {
if (!expectedCacheNamesSet.has(cacheName)) {
// If this cache name isn't present in the set of
// "expected" cache names, then delete it.
console.log("Deleting out of date cache:", cacheName);
return caches.delete(cacheName);
}
}),
),
),
);
});

self.addEventListener("fetch", (event) => {
console.log("Handling fetch event for", event.request.url);

event.respondWith(
caches.open(CURRENT_CACHES.font).then((cache) => {
return cache
.match(event.request)
.then((response) => {
if (response) {
// If there is an entry in the cache for event.request,
// then response will be defined and we can just return it.
// Note that in this example, only font resources are cached.
console.log(" Found response in cache:", response);

return response;
}

// Otherwise, if there is no entry in the cache for event.request,
// response will be undefined, and we need to fetch() the resource.
console.log(
" No response for %s found in cache. About to fetch " +
"from network…",
event.request.url,
);

// We call .clone() on the request since we might use it
// in a call to cache.put() later on.
// Both fetch() and cache.put() "consume" the request,
// so we need to make a copy.
// (see https://developer.mozilla.org/en-US/docs/Web/API/Request/clone)
return fetch(event.request.clone()).then((response) => {
console.log(
" Response for %s from network is: %O",
event.request.url,
response,
);

if (
response.status < 400 &&
response.headers.has("content-type") &&
response.headers.get("content-type").match(/^font\//i)
) {
// This avoids caching responses that we know are errors
// (i.e. HTTP status code of 4xx or 5xx).
// We also only want to cache responses that correspond
// to fonts, i.e. have a Content-Type response header that
// starts with "font/".
// Note that for opaque filtered responses
// https://fetch.spec.whatwg.org/#concept-filtered-response-opaque
// we can't access to the response headers, so this check will
// always fail and the font won't be cached.
// All of the Google Web Fonts are served from a domain that
// supports CORS, so that isn't an issue here.
// It is something to keep in mind if you're attempting
// to cache other resources from a cross-origin
// domain that doesn't support CORS, though!
console.log(" Caching the response to", event.request.url);
// We call .clone() on the response to save a copy of it
// to the cache. By doing so, we get to keep the original
// response object which we will return back to the controlled
// page.
// https://developer.mozilla.org/en-US/docs/Web/API/Request/clone
cache.put(event.request, response.clone());
} else {
console.log(" Not caching the response to", event.request.url);
}

// Return the original response object, which will be used to
// fulfill the resource request.
return response;
});
})
.catch((error) => {
// This catch() will handle exceptions that arise from the match()
// or fetch() operations.
// Note that a HTTP error response (e.g. 404) will NOT trigger
// an exception.
// It will return a normal response object that has the appropriate
// error code set.
console.error(" Error in fetch handler:", error);

throw error;
});
}),
);
});

✔️CacheStorage : CacheStorage is a web APIthat allows web applications to store data in the device cache. Cache is a temporary storage location for data that has been recently accessed. This can include images, CSS and JavaScript files, and page data. Cache is a powerful tool that can be used to improve the performance and availability of web applications. Here are some of the cache features:

  • local data storage ;
  • access to data from any tab of the same origin ;
  • data storage even when the user is not connected to the Internet ;
  • storage space management ;
  • data Lifetime Management ;

This example of service worker script waits for an InstallEvent to fire, then runs waitUntil to handle the install process for the app. This consists of calling CacheStorage.open to create a new cache, then using Cache.addAll to add a series of assets to it.

self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open("v1")
.then((cache) =>
cache.addAll([
"/",
"/index.html",
"/style.css",
"/app.js",
"/image-list.js",
"/star-wars-logo.jpg",
"/img/img1.jpg",
"/img/img2.jpg",
"/img/img3.jpg",
]),
),
);
});

self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// caches.match() always resolves
// but in case of success response will have value
if (response !== undefined) {
return response;
} else {
return fetch(event.request)
.then((response) => {
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
let responseClone = response.clone();

caches.open("v1").then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => caches.match("/img/img1.jpg"));
}
}),
);
});

✔️IndexedDB: IndexedDB is a web API that allows web applications to store data persistently in the browser. It is a NoSQL database system, which means that it does not require using a defined data structure. This makes IndexedDB very flexible and allows a wide variety of data to be stored. To use IndexedDB, a web application must first open a connection to a database. Once the connection is opened, the web application can create objects, tables and indexes. Objects can be stored in tables and tables can be indexed for fast searching.

Here are some of the features of IndexedDB:

  • Local data storage
  • Access to data from any tab of the same origin
  • Data storage even when the user is not connected to the Internet
  • Storage space management
  • Data Lifetime Management
  • Transaction Support
  • Support des index

Here is a sample code that shows how to open a connection to an IndexedDB database and create a table:

// Open a connection to the database
const db = indexedDB.open("myDatabase");

// Wait for the database to open
db.onupgradeneeded = (event) => {
// Create a table
const objectStore = db.createObjectStore("items");
objectStore.createIndex("name", "name", { unique: true });
};

// Wait for the database to be ready
db.onreadystatechange = (event) => {
if (db.readyState === "complete") {
// The database is ready, so we can start using it
}
};

Once the database connection is open, you can start creating objects, tables, and indexes. For example, the following code creates a called table itemsand an index on the column name:

const objectStore = db.createObjectStore("items");
objectStore.createIndex("name", "name", { unique: true });

Once the objects, tables, and indexes have been created, you can start storing data in the database. For example, the following code stores an object in the table items:

const item = {
name: "John Doe",
age: 30
};
objectStore.add(item);

You can also retrieve data from the database. For example, the following code retrieves all objects from the table items:

const cursor = objectStore.openCursor();
while (cursor.advance()) {
const item = cursor.value;
console.log(item.name, item.age);

IndexedDB is a powerful tool that can be used to improve the performance and availability of web applications. If you are developing a web application, I recommend you consider using IndexedDB.

In conclusion, here is what your final service-worker.js (sw.js) will look 👇

importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');
const VERSION = "version";
const CACHE_NAME = `pwa-${VERSION}`;

if (workbox) {
console.log(`Yay! Workbox is loaded 🎉`);

workbox.core.setCacheNameDetails({
prefix: 'yourPWAname',
suffix: 'version',
precache: 'precache',
runtime: 'run-time',
});

workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);

// Cache the Google Fonts stylesheets with a stale while revalidate strategy.
workbox.routing.registerRoute(
/^https:\/\/fonts\.googleapis\.com/,
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'google-fonts-stylesheets',
}),
);

// Cache the Google Fonts webfont files with a cache first strategy for 1 year.
workbox.routing.registerRoute(
/^https:\/\/fonts\.gstatic\.com/,
new workbox.strategies.CacheFirst({
cacheName: 'google-fonts-webfonts',
plugins: [
new workbox.cacheableResponse.Plugin({
statuses: [0, 200],
}),
new workbox.expiration.Plugin({
maxAgeSeconds: 60 * 60 * 24 * 365,
}),
],
}),
);
workbox.routing.registerRoute(
new RegExp('/css/'),
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'css-cache',
plugins: [
new workbox.expiration.Plugin({
// Only cache requests for a week
maxAgeSeconds: 15 * 24 * 60 * 60 * 365,
// Only cache requests.
maxEntries: 10,
}),
]
})
);

workbox.routing.registerRoute(
new RegExp('/js/'),
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'js-cache',
plugins: [
new workbox.expiration.Plugin({
// Only cache requests for a week
maxAgeSeconds: 15 * 24 * 60 * 60 * 365,
// Only cache requests.
maxEntries: 10,
}),
]
})
);
workbox.routing.registerRoute(
new RegExp('/media/'),
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'img-cache',
plugins: [
new workbox.expiration.Plugin({
// Only cache requests for a week
maxAgeSeconds: 15 * 24 * 60 * 60 * 365,
// Only cache requests.
maxEntries: 10,
}),
]
})
);


} else {
console.log(`Boo! Workbox didn't load 😬`);
}

self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open("v1")
.then((cache) =>
cache.addAll([
"/",
"/index.html",
"/style.css",
"/app.js",
"/image-list.js",
"/star-wars-logo.jpg",
"/img/img1.jpg",
"/img/img2.jpg",
"/img/img3.jpg",
]),
),
);
});

self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// caches.match() always resolves
// but in case of success response will have value
if (response !== undefined) {
return response;
} else {
return fetch(event.request)
.then((response) => {
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
let responseClone = response.clone();

caches.open("v1").then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => caches.match("/img/img1.jpg"));
}
}),
);
});

Congrats 🥳🥳👏👏 we managed to build a good service-worker file.

The next article we will talk about how to have a good workbox !

--

--

Louis Japheth Kouassi

I am passionate about Google Firebase, PWA, AI/ML & Flutter Techs. I like to learn, share and learn from others. GDG+TFUG Bassam Lead! Twitter: @LouisKouassii