One important aspect of PWAs is the concept of building an app that can be served offline. Up until this point of the book, we have made an application and enabled offline capabilities. We have seen the power of Service Worker, which has done most of the heavy lifting when it comes to storing static assets and dynamic content by leveraging Cache API. All in all, the achievement is significant compared to a traditional web application.
However, there is still room for improvement. Let’s imagine that you are building an application that communicates through REST API. Although Service Worker is facilitating to cache content and serve faster, it doesn’t help with a poor internet connection as soon as the network first strategy must be applied, and the respond and request have a long latency. Or what should we do with the application state or the app data set?
In PWA Note app, users’ experience is most likely disrupted because we keep them waiting until sending a message to the server is successfully completed if they have a poor internet connection. in fact, a delay of more than 10 seconds will often make users leave a site instantly. Slowness and lack of acceptable user experience could abruptly affect your business if it relies on the app.
In this chapter, I am going to explorer an approach that provides a consistent user experience whether the users’ devices have no connectively, limited connectivity, or great connectively. This model reduces latency down to zero as it provides access to content stored directly on the device and synchronizes data in all users’ devices over HTTP.
Offline Storage
sessionStorage maintains a separate storage area for each given origin that’s available for the duration of the page session (as long as the browser is open, including page reloads and restores).
localStorage does the same thing but persists even when the browser is closed and reopened.
- 1.
You need to serialize and deserialize data when you want to store (only strings).
- 2.
API is synchronous, which means it blocks the application and has no Web Worker support.
WebSQL is asynchronous (callback-based); however, it also has no Web Worker support and was rejected by Firefox and Edge but is in Chrome and Safari. It’s also depreciated.
File System API is asynchronous too (callback-based) and does work in Web Workers and Windows (albeit with a synchronous API). Unfortunately, it doesn’t have much interest outside of Chrome and is sandboxed (meaning you don’t get native file access).
File API is being improved over in the File and Directory Entries API and File API specs. A File API library exists and for file saving, I’ve been using FileSaver.js as a stopgap. The writable-files proposal may eventually give us a better standards-track solution for seamless, local file interaction.
IndexedDB is a key-value pair NoSQL database and supports large scale storage (up to 20%–50% of hard drive capacity) and supports many data types like number, string, JSON, blob, and so on. As it is asynchronous, it can be used everywhere including Web Workers and is widely supported in the browsers.
Cache API provides a storage mechanism for Request / Response object pairs that are cached, for example, as part of the Service Worker life cycle. Note that the Cache interface is exposed to window scopes as well as workers.
As we have seen, it seems the best options are IndexedDB1 and Cache API. A combination of both APIs makes it much more reliable and provides a better user experience. We have used Cache API to store URL addressable resources such as static files and request and respond from REST APIs. There are no hard rules how to use and architect your application to leverage these APIs. Some applications might be sufficiently simple that they can just use the Cache API alone, while others may find it valuable to partially cache their JSON payloads in IDB so that in browsers without Cache API support, you still get the benefit of some local caching during the session.
Note
IndexedDB API is powerful but may seem too complicated for simple cases. I recommend trying libraries such as LocalForage, Dexie.js, zangoDB, PouchDB, LoxiJs, JsStore, IDB, LokiJs that help to wrap IndexedDB APIs, which make it more programmer friendly. Also, this API was buggy and slow in Safari 10; therefore some of these libraries implemented a fall back to WebSQL in Safari as opposed to indexedDB to gain a better performance. Although this issue was resolved and IndexedDB is stable in all major browsers, if your apps target older browsers for some certain reasons, you may need to use suggested libraries: for example, Localforage
For the network resources necessary to load your app while offline, use the Cache.
For all other data, use IndexedDB, for instance, application state and data set are the best candidates to be stored in IndexedDB.
Offline First Approach
One issue with this approach is that a flaky or nonexistent internet connection may interrupt the user experience and lead to unreliable performance. To fix that, we have used Service Worker and will leverage other storage techniques to substantially improve the user experience in all situations, including a perfect wireless environment.
Service Worker can intercept request between the client and server if needed. We can even think of how to synchronize our data with the server.
Note
Thanks to Background Sync event in Service Worker, it’s easily possible to resolve synchronization. I will explore the sync event in Chapter 14 when we are implementing Workbox because this feature is not available in Angular Service Worker as of now (Angular 7.1).
Let’s experiment with the offline first database approach in PWA Note application and see how it works in action.
Implement Offline First Approach with Sync Server
We have figured out that IndexedDB is what we need to use in a client app. The next hurdle is figuring out how to store and sync the app’s data and state. Offline syncing is a bit more challenging than it looks. I believe one of the best solutions to overcome this obstacle is to use PouchDB .2 Keep in mind, you are not limited to this solution, and you may need to either implement your own logic for your application or use another third party.3 All in all, the goal is to implement offline first cache for storing data and sync back to the server accordingly.
Note
PouchDB is an open source JavaScript database inspired by Apache CouchDB4 that is designed to run well within the browser. PouchDB was created to help web developers build applications that work as well offline as they do online. 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 log in.
You can use PouchDB without the sync feature too, but for the sake of offline capability, I enable the sync and offline features in PouchDB.
The pouchdb-browser preset contains the version of PouchDB that is designed for the browser. In particular, it ships with the IndexedDB and WebSQL adapters as its default adapters. It also contains the replication, HTTP, and map/reduce plugins. Use this preset if you only want to use PouchDB in the browser, and don’t want to use it in Node.js. (e.g., to avoid installing LevelDB.)
If you clone the project repository and want to see the example codes, first install npm packages and then npm run pouchdb-server
Note
If you see an error due to undefined global object in console, please add (window as any).global = window; at the bottom of Polyfills.ts.6
As you have been shown in Figure 9-5, each document has an _id and _rev property that is being added automatically. The change object contains all the necessary information that we can use in our app logic to manipulate the data.
Note
The rev field in the response indicates a revision of the document. Each document has a field by the name _rev. Every time a document is updated, the _rev field of the document is changed. Each revision points to its previous revision. PouchDB maintains a history of each document (much like git). _rev allows PouchDB and CouchDB to elegantly handle conflicts, among its other benefits.
So far so good; you’ll notice that there will be zero latency to show or add a note, since the content is going to be added to the cache first and then will be synced back with the server. Therefore, our user will not notice the latency between the cache and server.
What if our user goes offline? Let’s test it out. We’ll disconnect the network by checking offline in Chrome and then will try to delete a note from Safari where it’s online still and add a note from Chrome browser, which is offline (see Figures 9-7 and 9-8).
Note
PouchDB has two types of data: documents and attachments.
Documents
As in CouchDB, the documents you store must be serializable as JSON.
Attachments
PouchDB also supports attachments, which are the most efficient way to store binary data. Attachments may either be supplied as base64-encoded strings or as Blob objects.
There has been no interruption in the user experience, and there has been fast performance and reliable data and synchronization – isn't it amazing?
As said, PouchDB is one way to the implement offline first approach. Depending on your application and requirements, you may use different libraries or even your own implementation where you use IndexedDB APIs directly.
Implement Persistent Data with Angular Firebase
Next, we need to make sure we are using Angular Firestore APIs.
Run the application and disconnect from the network. You can add a note even though you are offline; and as soon as you come back online, the data will sync back to Firestore.
User Interface Considerations
- 1.
Change the header and footer color to some other colors that indicate they are offline; for instance, in the Note app, we can gray out the blue header when the user is offline.
- 2.
Show a notification or a popup when the user is offline; for instance, when the user is adding a note in Note PWA app, we can show a message that you are offline, but we will sync back data to server as soon as you are online.
- 3.
Display an icon or other indication that clearly shows even though a note has been added, it’s not synced with the server yet and only exists on the user local device.
- 4.
Resolve conflicts based on user decision; for instance, a user may edit a note in different devices at once when all devices are offline, and when all devices come online again, there might be conflicts between each revision. In this case, it’s a good practice to show our user a notification and tell them that there are different revisions based on their edit; therefore, they can select which update is the one that needs to be applied.
These are just a few ideas. You may have better ideas based on your app. It is important to enhance the UIs along with adding more functionalities and features to boost the user experience.
As you see, you can write your own logic based on how the network information changes.
Note
If you want to see and run all the examples and codes on your machine, simply clone https://github.com/mhadaily/awesome-apress-pwa.git , then go to chapter09. For pouchdb implementation, you’ll find 01-pouchdb; go to folder and install all packages first by running npm install and then run both app and pouchdb-server by running npm start and npm run pouchdb-server respectively. For Firestore implementation, go to 02-firebase-presistent-db, run npm install and npm start respectively.
Summary
One main aspect of PWAs is to enhance the user experience. Providing an offline experience – whether with a flaky connection in transport or being offline in the airplane – is invaluable to boost user satisfaction and improve the app’s performance.
To support a meaningful experience in an offline case, not only we should cache our static assets and requests and responses, but also storing data on the client side seems essential. By rethinking how to architect an application in the front end and make it offline – first by leveraging browser offline storage, like IndexedDB, with one of the libraries available (PouchDB), the app has been moved to the next level.