Sign Up
Hero

Introduction to Progressive Web Apps (Instant Loading) - Part 2

Progressive Web Apps are the future. Learn how to make your mobile web app native-like by making it work offline and load instantly.


TL;DR: Web development has evolved significantly over the years allowing developers to deploy a website or web application and serve millions of people around the globe within minutes. With just a browser, a user can put in a URL and access a web application. With, Progressive Web Apps, developers can deliver amazing app-like experiences to users using modern web technologies. In the first part of this tutorial we set up our progressive web app, cached the pages and made it work partially offline. This time, we'll make it load instantly and work offline fully.


Recap and Introduction to Part 2

In Introduction to Progressive Web Apps (Offline First), we discussed how a typical progressive web application should look like and also introduced the service worker. So far, we've cached the application shell. The index and latest pages of our web app now load offline. They also load faster on repeated visits. Towards the end of the first part of this tutorial, we were able to load the latest page offline but couldn't get dynamic data to display when the user is offline.

This tutorial will cover:

  • Caching the App data on the latest page to be displayed to the user when offline
  • Using localStorage to store the App data
  • Flushing out old App data and fetching updated data when the user is connected to the internet

Offline Storage

When building progressive web apps, there are various storage mechanisms to consider like so:

  • IndexedDB: This is a transactional JavaScript based database system for client-side storage of data. This database employs the use of indexes to enable high performance searches of the data stored in it. IndexedDB exposes an asynchronous API that supposedly avoids blocking the DOM, but some research has shown that in some cases it is blocking. I recommend that you use libraries when working with IndexedDB because manipulating it in vanilla JavaScript can be very verbose and complex. Examples of good libraries are localForage, idb and idb-keyval.

    IndexedDB browser support

  • Cache API: This is best for storing url addressable resources. Works with Service worker really well.
  • PouchDB: Open Source JavaScript database inspired by CouchDB. It enables applications to store data locally while offline, then synchronize it with CouchDB and compatible servers when the application is back online, keeping the user's data in sync no matter where they next login. PouchDB supports all modern browsers, using IndexedDB under the hood and falling back to WebSQL where IndexedDB isn't supported. Supported in Firefox 29+ (Including Firefox OS and Firefox for Android), Chrome 30+, Safari 5+, Internet Explorer 10+, Opera 21+, Android 4.0+, iOS 7.1+ and Windows Phone 8+.
  • Web Storage e.g localStorage: It is synchronous and can block the DOM. The usage is capped at 5MB in most browsers. It has a simple API for storing data and it uses key-value pairs.

    Web Storage browser support

  • WebSQL: This is a relational database solution for browsers. It has been deprecated and the specification is no longer maintained. So, browsers may not support it in the future.

Mobile Storage Quota

Addy Osmani has a comprehensive resource on Offline storage for progressive web apps. You should really check it out!

According to PouchDB maintainer, Nolan Lawson, do well to ask yourself these questions when you are using a database:

  • Is this database in-memory or on-disk(PouchDB, IndexedDB)?
  • What needs to be stored on disk? What data should survive the application being closed or crashing?
  • What needs to be indexed in order to perform fast queries? Can I use an in-memory index instead of going to disk?
  • How should I structure my in-memory data relative to my database data? What’s my strategy for mapping between the two?
  • What are the query needs of my app? Does a summary view really need to fetch the full data, or can it just fetch the little bit it needs? Can I lazy-load anything?

You can check out how to think about databases to give you a broader knowledge on the subject matter.

Let's Implement Instant Loading

For our web app, we'll use localStorage . I recommend that you don't use localStorage for production apps because of the limitations I highlighted earlier in this tutorial. The app we are building is a very simple one, so localStorage will work fine.

Open up your js/latest.js file. We will update the fetchCommits function to store the data it fetches from the Github API in localStorage like so:

 function fetchCommits() {
    var url = 'https://api.github.com/repos/unicodeveloper/resources-i-like/commits';

    fetch(url)
    .then(function(fetchResponse){ 
      return fetchResponse.json();
    })
    .then(function(response) {
        console.log("Response from Github", response);

        var commitData = {};

        for (var i = 0; i < posData.length; i++) {
          commitData[posData[i]] = {
            message: response[i].commit.message,
            author: response[i].commit.author.name,
            time: response[i].commit.author.date,
            link: response[i].html_url
          };
        }

        localStorage.setItem('commitData', JSON.stringify(commitData));

        for (var i = 0; i < commitContainer.length; i++) {

          container.querySelector("" + commitContainer[i]).innerHTML = 
          "<h4> Message: " + response[i].commit.message + "</h4>" +
          "<h4> Author: " + response[i].commit.author.name + "</h4>" +
          "<h4> Time committed: " + (new Date(response[i].commit.author.date)).toUTCString() +  "</h4>" +
          "<h4>" + "<a href='" + response[i].html_url + "'>Click me to see more!</a>"  + "</h4>";

        }

        app.spinner.setAttribute('hidden', true); // hide spinner
    })
    .catch(function (error) {
      console.error(error);
    });
};

With this piece of code above, on first page load, the commit data will be stored in localStorage. Now let's write another function to retrieve the data from localStorage like so:

  // Get the commits Data from the Web Storage
  function fetchCommitsFromLocalStorage(data) {
    var localData = JSON.parse(data);

    app.spinner.setAttribute('hidden', true); //hide spinner

    for (var i = 0; i < commitContainer.length; i++) {

      container.querySelector("" + commitContainer[i]).innerHTML = 
      "<h4> Message: " + localData[posData[i]].message + "</h4>" +
      "<h4> Author: " + localData[posData[i]].author + "</h4>" +
      "<h4> Time committed: " + (new Date(localData[posData[i]].time)).toUTCString() +  "</h4>" +
      "<h4>" + "<a href='" + localData[posData[i]].link + "'>Click me to see more!</a>"  + "</h4>";

    }
  };

This piece of code fetches data from localStorage and appends it to the DOM.

Now, we need a conditional to know when to call the fetchCommits and fetchCommitsFromLocalStorage function.

The updated latest.js file should look like so:

latest.js

(function() {
  'use strict';

  var app = {
    spinner: document.querySelector('.loader')
  };

  var container = document.querySelector('.container');
  var commitContainer = ['.first', '.second', '.third', '.fourth', '.fifth'];
  var posData = ['first', 'second', 'third', 'fourth', 'fifth'];

  // Check that localStorage is both supported and available
  function storageAvailable(type) {
    try {
      var storage = window[type],
        x = '__storage_test__';
      storage.setItem(x, x);
      storage.removeItem(x);
      return true;
    }
    catch(e) {
      return false;
    }
  }

  // Get Commit Data from Github API
  function fetchCommits() {
    var url = 'https://api.github.com/repos/unicodeveloper/resources-i-like/commits';

    fetch(url)
    .then(function(fetchResponse){ 
      return fetchResponse.json();
    })
    .then(function(response) {
        console.log("Response from Github", response);

        var commitData = {};

        for (var i = 0; i < posData.length; i++) {
          commitData[posData[i]] = {
            message: response[i].commit.message,
            author: response[i].commit.author.name,
            time: response[i].commit.author.date,
            link: response[i].html_url
          };
        }

        localStorage.setItem('commitData', JSON.stringify(commitData));

        for (var i = 0; i < commitContainer.length; i++) {

          container.querySelector("" + commitContainer[i]).innerHTML = 
          "<h4> Message: " + response[i].commit.message + "</h4>" +
          "<h4> Author: " + response[i].commit.author.name + "</h4>" +
          "<h4> Time committed: " + (new Date(response[i].commit.author.date)).toUTCString() +  "</h4>" +
          "<h4>" + "<a href='" + response[i].html_url + "'>Click me to see more!</a>"  + "</h4>";

        }

        app.spinner.setAttribute('hidden', true); // hide spinner
      })
      .catch(function (error) {
        console.error(error);
      });
  };

  // Get the commits Data from the Web Storage
  function fetchCommitsFromLocalStorage(data) {
    var localData = JSON.parse(data);

    app.spinner.setAttribute('hidden', true); //hide spinner

    for (var i = 0; i < commitContainer.length; i++) {

      container.querySelector("" + commitContainer[i]).innerHTML = 
      "<h4> Message: " + localData[posData[i]].message + "</h4>" +
      "<h4> Author: " + localData[posData[i]].author + "</h4>" +
      "<h4> Time committed: " + (new Date(localData[posData[i]].time)).toUTCString() +  "</h4>" +
      "<h4>" + "<a href='" + localData[posData[i]].link + "'>Click me to see more!</a>"  + "</h4>";

    }
  };

  if (storageAvailable('localStorage')) {
    if (localStorage.getItem('commitData') === null) {
      /* The user is using the app for the first time, or the user has not
       * saved any commit data, so show the user some fake data.
       */
      fetchCommits();
      console.log("Fetch from API");
    } else {
      fetchCommitsFromLocalStorage(localStorage.getItem('commitData'));
      console.log("Fetch from Local Storage");
    }   
  }
  else {
    toast("We can't cache your app data yet..");
  }
})();

In the piece of code above, we are checking if the browser supports localStorage and if it does, we go ahead to check if the commit data has been cached. If it has not been cached, we fetch, display and cache the app's commit data.

Now, reload the browser again, make sure you do a hard, clear-cache, reload else we won't see the result of our code changes.

Now, go offline and load the latest page. What happens?

Yaaay!! it loads the data without any problem.

Check the DevTools, you'll see the data been stored in localStorage.

Store Data Locally

Just look at the speed at which it loads when the user is offline....OMG!!!

Loads from Service Worker when user is offline

One More Thing

Now, we can make the app load instantly by fetching data from the localStorage. How do we get fresh updated data? We need a way of still getting fresh data especially when the user is online.

It's simple. Let's add a refresh button that triggers a request to GitHub to get the most recent data.

Open up your latest.html file and add this code for the refresh button within the <header> tag.

<button id="butRefresh" class="headerButton" aria-label="Refresh"></button>

So the <header> tag should look like this after adding the button:

<header>
  <span class="header__icon">
    <svg class="menu__icon no--select" width="24px" height="24px" viewBox="0 0 48 48" fill="#fff">
      <path d="M6 36h36v-4H6v4zm0-10h36v-4H6v4zm0-14v4h36v-4H6z"></path>
    </svg>
  </span>
  <span class="header__title no--select">PWA - Commits</span>
  <button id="butRefresh" class="headerButton" aria-label="Refresh"></button>
</header>

Finally, let's attach a click event to the button and add functionality to it. Open your latest.js and add this code at the top like so:

document.getElementById('butRefresh').addEventListener('click', function() {
    // Get fresh, updated data from GitHub whenever you are clicked
    toast('Fetching latest data...');
    fetchCommits();
    console.log("Getting fresh data!!!");
});

Clear your cache and reload the app. Now, your latest.html page should look like so:

Get Updated data

Anytime users need the most recent data, they can just click on the refresh button.

Aside: Easy Authentication with Auth0

You can use Auth0 Lock for your progressive web app. With Lock, showing a login screen is as simple as including the auth0-lock library and then calling it in your app like so:

// Initiating our Auth0Lock
var lock = new Auth0Lock(
  'YOUR_CLIENT_ID',
  'YOUR_AUTH0_DOMAIN'
);

// Listening for the authenticated event
lock.on("authenticated", function(authResult) {
  // Use the token in authResult to getProfile() and save it to localStorage
  lock.getProfile(authResult.idToken, function(error, profile) {
    if (error) {
      // Handle error
      return;
    }

    localStorage.setItem('idToken', authResult.idToken);
    localStorage.setItem('profile', JSON.stringify(profile));
  });
});

Note: If you want to use Auth0 authentication to authorize API requests, note that you'll need to use a different flow depending on your use case. Auth0 idToken should only be used on the client-side. Access tokens should be used to authorize APIs. You can read more about making API calls with Auth0 here.

Implementing Lock

document.getElementById('btn-login').addEventListener('click', function() {
  lock.show();
});

Showing Lock

Auth0 Lock Screen

In the case of an offline-first app, authenticating the user against a remote database won't be possible when network connectivity is lost. However, with service workers, you have full control over which pages and scripts are loaded when the user is offline. This means you can configure your offline.html file to display a useful message stating the user needs to regain connectivity to login again instead of displaying the Lock login screen.

Conclusion

In this article, we were able to make our app load instantly and work offline. We were able to cache our dynamic data and serve the user the cached data when offline.

In the final part of this tutorial, we will cover how to enable push notifications, add web application manifest and our app to a user's homescreen.