Native mobile applications have set a high benchmark for the web by providing a rich and reliable internet experience for users. However, in terms of discoverability, the web has been the undefeated leader for over a decade now. With more and more browser vendors showing serious intent to bridge this gap, the ‘rich versus reach’ battle is finally lessening. Progressive web apps (PWAs), a concept born out of the need to push the boundaries of the web, have opened a playground where developers can reap the benefits of both worlds. According to Google, a PWA has the following characteristics:

Instant-loading: Users shouldn’t wait too long for your app to load. Just like native apps, they should see some relevant information as soon as they open the app

Safe: User content is secure and cannot be tampered with

App-like: They emulate app behaviours, such as support for push notifications, and should be able to leverage device sensors

Discoverable and linkable: PWAs are crawlable by search engines and don’t require any special discovery marketplace. Thanks to the way the web is built, this comes out of the box!

Progressive: These apps are built with progressive enhancement as a core tenet

Installable: Users can ‘install’ PWAs on their devices without going through complex installation processes. Features like ‘Add to homescreen’ and splash screen support make these apps installable and provide consistent user experience, just like any other native app

What will we build ?

We will build a very simple PWA that shows all the posts from Reddit’s front page, using Reddit’s public API. This app, like any other progressive web app, should run in all network conditions, it should be installable and should run as a standalone app. It should also load up instantly.

Application Architecture

For the first request, we will make a network call and store the response (in our case, the app shell) in the cache. For the subsequent requests, we will be serving our app shell cached content. The Service Worker will act as a proxy between our application and the server, and will keep on refreshing the cache each time we request a resource.

Application shells are containers for your web app. These shells:

Should have the minimal HTML, CSS and JavaScript needed to render static content, along with your app’s bootstrap code

Define a loading state for your application

Should load fast and be cached

Let’s create a basic shell for our app. We will first decide on a loading state. For our Reddit posts, we will create placeholders while the posts are being fetched.

Here’s the complete code for our shell:

<html> <head> <title>PWA Demo App</title> <link rel="stylesheet" href="/public/css/styles.css"/> </head> <body> <style> /*Add your inline styles here..*/ </style> <div class="content"> <div class="header">Reddit - Top posts</div> <div id="offline-status-container">You are currently browsing offline.</div> <div id="post-content"> <div class="post-placeholder"> <div class="placeholder-long"></div> <div class="placeholder-short"></div> </div> <div class="post-placeholder"> <div class="placeholder-long"></div> <div class="placeholder-short"></div> </div> </div> </div> <script src="/public/js/app.js"></script> </body> </html> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 < html > < head > < title > PWA Demo App < / title > < link rel = "stylesheet" href = "/public/css/styles.css" / > < / head > < body > <style> /*Add your inline styles here..*/ </style> < div class = "content" > < div class = "header" > Reddit - Top posts < / div > < div id = "offline-status-container" > You are currently browsing offline . < / div > < div id = "post-content" > < div class = "post-placeholder" > < div class = "placeholder-long" > < / div > < div class = "placeholder-short" > < / div > < / div > < div class = "post-placeholder" > < div class = "placeholder-long" > < / div > < div class = "placeholder-short" > < / div > < / div > < / div > < / div > <script src = "/public/js/app.js" > </script> < / body > < / html >

The shell will look something like the image opposite. From this point, we just need to create a lightweight server (for example using Express) to serve our files locally.

Service Worker

To make our web apps progressive in real sense, we need them to load in all conditions and not show an offline screen to the user when the network is not available. Thanks to Service Workers, we can now control how our applications work in poor network conditions. Service Workers are worker scripts that run in the background and can listen to or intercept network calls made by your app.

Before we begin, create a sw.js file and leave it empty – we will come back and write our caching logic in this file. Now let’s add this Service Worker initialisation snippet to our app shell:

<script> if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'). then(function(registration) { // Registration was successful console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function(err) { // registration failed :( console.log('ServiceWorker registration failed: ', err); }); } </script> 1 2 3 4 5 6 7 8 9 10 11 12 <script> if ( 'serviceWorker' in navigator ) { navigator . serviceWorker . register ( '/sw.js' ) . then ( function ( registration ) { // Registration was successful console . log ( 'ServiceWorker registration successful with scope: ' , registration . scope ) ; } ) . catch ( function ( err ) { // registration failed :( console . log ( 'ServiceWorker registration failed: ' , err ) ; } ) ; } </script>

This script checks if ServiceWorker is supported by the browser, and then registers the Service Worker file (in our case /sw.js ) you pass to the register function. There are a couple of things to note here. First, the registered Service Worker should be served only through HTTPS. However, localhost is whitelisted for development purposes. Second, Service Worker should always be served from the same origin as your web app.

Adding the Manifest

We will now work on making our web app installable. For this, we need icons for our app and a file called manifest.json. This file holds the centralised application configuration and metadata. User-agents use this file to recognise your app as an installable web app.

In this JSON config you can define the following:

Application name: Used to display your application name beneath the icon on your homescreen

Display mode: This will control how your application will be presented to the users. Display mode can have one of the following values: fullscreen , standalone , minimal-ui or browser

Icons: These will represent your app on the splash screen and the homescreen icon. The last one is picked by default, falls back to the second-last one and so on

Orientation: You can select the default orientation for your application: either portrait or landscape

Start URL: The URL you want to load when the user launches your application

Theme colour: This sets the default theme colour and also changes the status bar colour in Android

Background colour: Sets the background colour of your application before the static assets defining the background colour have loaded. Your application style sheet takes over after it is processed

To register a manifest.json file for your application, drop a file in the root folder with this name and add the following tag in the application shell:

<link rel="manifest" href="./manifest.json"> 1 < link rel = "manifest" href = "./manifest.json" >

And add this snippet in server.js :

app.get('/manifest.json', function(req, res) { res.set('Content-Type', 'application/json'); res.end(fs.readFileSync(path.resolve(__dirname) + 'manifest.json', 'utf8')); }); 1 2 3 4 app . get ( '/manifest.json' , function ( req , res ) { res . set ( 'Content-Type' , 'application/json' ) ; res . end ( fs . readFileSync ( path . resolve ( __dirname ) + 'manifest.json' , 'utf8' ) ) ; } ) ;

Then create a manifest.json in your root directory and add the following configuration:

{ "name": "Reddit PWA Demo", "short_name": "PWA Demo", "icons": [ { "src": "logo.png", "sizes": "192x192", "type": "image/png" } ], "start_url": "/", "orientation": "portrait", "display": "standalone", "theme_color": "#3F51B5", "background_color": "#E8EAF6" } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "name" : "Reddit PWA Demo" , "short_name" : "PWA Demo" , "icons" : [ { "src" : "logo.png" , "sizes" : "192x192" , "type" : "image/png" } ] , "start_url" : "/" , "orientation" : "portrait" , "display" : "standalone" , "theme_color" : "#3F51B5" , "background_color" : "#E8EAF6" }

Going Offline

Now we have made our web app installable, let’s make it work offline or on flaky network connections using some Service Worker magic! But before we start writing the Service Worker code for our app, let’s decide a caching strategy.

There are several strategies to cache your data using Service Workers. Some of them are:

Network-first: Always try to first get data from the original source, and fall back to cache if the network fails. When the network call succeeds, you update the cache so you have some data to show to users when there is no network access. This is preferred for cases where you always need to show dynamic data (like price, stock values and so on)

Cache-first: This strategy works well with the offline-first approach of building apps. You first try to fetch data from cache, and then fall back to the network if the data is not present in the cache. You also fire a network call to update the cache for subsequent access

Fastest: Request resources from both network and the cache in parallel, but honour only the fastest returned response

Cache-only: Always fetch from cache. If data is not present in cache, first update the cache and then serve data

Network-only: No caching, always fetch from network

It’s important our application works when it is offline and we do not mind if we show stale data to the users for our demo, so we will be choosing the cache-first strategy.

It’s worth taking some time to decide on the right caching strategy for your app, as it vastly affects the experience you are going to provide for to your users. Plus, stale data can land you in trouble if you are serving information that is critical and volatile (such as prices or scores) to the users.

Let’s write code to cache our resources through Service Workers and the Cache API. Inside sw.js , add this snippet to create a new cache through the browser caching API, and add our resources to the cache.

var CACHE_NAME = 'pwa-demo-cache-v1'; var urlsToCache = [ '/', '/public/css/styles.css', '/public/js/app.js', 'https://www.reddit.com/.json' ]; self.addEventListener('install', function(event) { event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(urlsToCache); }) ); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var CACHE_NAME = 'pwa-demo-cache-v1' ; var urlsToCache = [ '/' , '/public/css/styles.css' , '/public/js/app.js' , 'https://www.reddit.com/.json' ] ; self . addEventListener ( 'install' , function ( event ) { event . waitUntil ( caches . open ( CACHE_NAME ) . then ( function ( cache ) { return cache . addAll ( urlsToCache ) ; } ) ) ; } ) ;

It’s advisable to version your cache. This will help when it comes to purging it later when you want a fresh set of resources to be cached.

Next, we add logic to listen to the browser fetch event and intercept the network calls to serve data from cache (if it’s available). We also want to fire a network call to update the cache with fresh data.

self.addEventListener('fetch', function(event) { event.respondWith( caches.open(CACHE_NAME).then(function(cache) { return cache.match(event.request). then(function(response) { var fetchPromise = fetch(event.request). then(function(networkResponse) { cache.put(event.request, networkResponse.clone()); return networkResponse; }); return response || fetchPromise; }) }) ); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 self . addEventListener ( 'fetch' , function ( event ) { event . respondWith ( caches . open ( CACHE_NAME ) . then ( function ( cache ) { return cache . match ( event . request ) . then ( function ( response ) { var fetchPromise = fetch ( event . request ) . then ( function ( networkResponse ) { cache . put ( event . request , networkResponse . clone ( ) ) ; return networkResponse ; } ) ; return response || fetchPromise ; } ) } ) ) ; } ) ;

Offline Notification

It’s always a good idea to let the users know when they go offline. To display an offline notification, create a hidden element in the app shell and add the following script just before the closing body tag:

function handleOnlineStatus() { if (navigator.onLine) { document.getElementById('offline-status-container'). style.display = "none"; } else { document.getElementById('offline-status-container'). style.display = "block"; } } window.addEventListener('load', function(){ window.addEventListener('online', handleOnlineStatus); window.addEventListener('offline', handleOnlineStatus); }); 1 2 3 4 5 6 7 8 9 10 11 function handleOnlineStatus ( ) { if ( navigator . onLine ) { document . getElementById ( 'offline-status-container' ) . style . display = "none" ; } else { document . getElementById ( 'offline-status-container' ) . style . display = "block" ; } } window . addEventListener ( 'load' , function ( ) { window . addEventListener ( 'online' , handleOnlineStatus ) ; window . addEventListener ( 'offline' , handleOnlineStatus ) ; } ) ;

Running our example

When you run your app for the first time in your browser, you will see the loading state defined in the application shell, and when the data is finally fetched you will see the complete rendered state of our application. If you disable the network, you should see a bar (similar to the one in the image below)

telling you that you are browsing the app offline, and if you reload the page you should be able to look at content that was loaded the last time. That means your app now works offline! This is a good time to check out the timeline in the Network tab in Chrome DevTools. You will see the resources being fetched from the Service Worker for the calls following the first load. It should look something like the image above.

Recap

We just created our basic PWA, but there is a lot more you can do to make your web apps more interactive. I’d encourage you to explore the new browser APIs like Push Notification and keep pushing the boundaries of what the traditional web has delivered. PWAs will mark an important milestone in the history of web. The web is finally done playing catch up!