Background Sync - PWA’s Backbone

Background sync in PWA limits the time spent waiting for data and delivers a smooth and responsive online experience. First, this post will show how background sync works in a Progressive Web App, where every user action causes a request to be sent from the client to the server. Then, we’ll see how background sync ensures that the application records all actions continuously.

Progressive Web Applications have a fast, responsive, and engaging user experience. However, mobile users have different performance expectations from the app. Without data-in-the-background support, the app will be visibly sluggish and display errors. Background sync allows you to store data locally on the user’s device to access them while offline. The background sync functionality is available and supported by Google Chrome, and other browsers (Safari, Edge, Firefox) are under development.

The life cycle of a background sync

Unlike others, the lifecycle of background sync allows developers to schedule updates to multiple websites at once asynchronously. This API uses Service Worker, too.

Initially, the app requests, but the service worker steps in instead of the request being processed immediately. The service worker checks if the user has internet access. If they do, then the app will send the request. If not, the service worker will wait until the user gets internet access and then send the request. Meanwhile, it fetches data out of IndexedDB. Best of all, the background sync will go ahead and send the request irrespective if the user has moved away from the original page.

Each time the app registers changes within the service worker, it will notify all registered background sync tasks about those changes using BackgroundSyncManager.update().

How does PWA benefit from Service Worker?

Background sync is only possible with service workers. Although a service worker isn’t solely responsible for enabling background sync, getting these features to work is the backbone and foundation of getting these features. Service workers run in the background, listening for connections from your browser. They are installed from online assets and work with pre-cached data to keep the phone running smoothly and enable data transmission without affecting battery usage or internet consumption.

Service Worker is a client-side technology that enables you to run JavaScript code in the web page’s context, independent of the page’s origin. It brings an ability to detect a network connection, intercept and cache network requests, or respond with a local response from the cache.

A typical Service Worker process is as below,

  • Offline Support: The service worker can detect when the user is offline; it can then respond with offline assets. Or it could check if new updates are available for your application or prompt the user to install them.
  • Implementing background sync: The Service worker will utilize the background sync web API, which will use the navigator.online to detect a stable internet connection. If it is offline, the API will store the data in the web storage. Once the connection is alive, it will sync data from web storage, send a request to the server, and delete that record from web storage. Background sync is especially useful for applications that frequently get large amounts of data from servers, such as news apps that fetch the latest headlines or chat apps that update the presence of users.

IndexedDB: Web Storage

Various Web Storage APIs are offered, such as Session Storage, Local Storage, and IndexedDB, and each has its advantages and disadvantages.

In a typical web application, you use the IndexedDB API to store and retrieve information. IndexedDB is a built-in database, much more potent than localStorage. You may know that IndexedDB stores data on a per-origin basis, but did you know that there’s no need to keep your website open for this API work?

For implementing IndexedDB, there are various methods through which we can open the database and can read and write the object-store. Once the navigator.online returns true, we can use another method to open the object store and execute the respective action, and lastly, complete the transaction. The Google Developers site has good background information on IndexedDB (Working with IndexedDB | Web)

Implementation of Background Sync and IndexedDB in Application

We will be demonstrating an app through which we can send messages even when offline. We will begin by explaining IndexedDB and creating a small framework around it to use it with promises. Post this, we will integrate IndexedDB with background sync into our application. Below are the steps to create your application

Register a sync

Firstly, we need to register a sync event in index.html through which we’ll be syncing the data we want, i.e., messages. The Service worker is used as an event target, enabling it to work when the page isn’t open.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if ( 'serviceWorker' in navigator ) {
navigator. serviceWorker . register ( './serviceworker.js' )
. then (() = > navigator. serviceWorker . ready )
. then ( registration = > {
if ( 'SyncManager' in window ) {
registration. sync . register ( 'sync-messages' )
}
})
}
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./serviceworker.js') .then(() => navigator.serviceWorker.ready) .then(registration => { if ('SyncManager' in window) { registration.sync.register('sync-messages') } }) }
if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('./serviceworker.js')
        .then(() => navigator.serviceWorker.ready)
        .then(registration => {
          if ('SyncManager' in window) {
            registration.sync.register('sync-messages')
          }
        })
}

Adding listener for a sync event

In the service worker, we need to add a listener to sync events. Whenever a specific event fires, the respective sync event captures it. The event.tag property validates this event. We can then run a function inside the event.waitUntil() to communicate with the API to send a message or fetch data from IndexedDB for the update.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
self. addEventListener ( 'sync' , event = > {
if ( event. tag == 'sync-messages' ) {
event. waitUntil ( 'syncMessages' ) ;
}
}
self.addEventListener('sync', event => { if (event.tag =='sync-messages') { event.waitUntil('syncMessages'); } }
self.addEventListener('sync', event => {
   if (event.tag =='sync-messages') {
      event.waitUntil('syncMessages');
   }
}

Send messages to the API

Through the function, we can send a message to the API. In this class, if the API request fails due to the lack of an internet connection, IndexedDB saves our request and detects this error. Inside the service, we will create a message, i.e., createMessages(), which uses the fetch API to send the request. Let’s see how we can implement the same MessagesService.js file.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class MessagesService {
async createMessages ( messages ) {
retur fetch ( 'https://api/messages' , {
method: 'POST' ,
body: JSON. stringify ({
content: messages. content ,
author_id: messages. author_id
}) ,
headers: {
'Content-Types' : 'application/json'
}
}) . catch ( error = > {
this . saveMessagesInOffline ( messages ) ;
throw error;
}) ;
}
...
}
class MessagesService { async createMessages(messages) { retur fetch('https://api/messages', { method: 'POST', body: JSON.stringify({ content: messages.content, author_id: messages.author_id }), headers: { 'Content-Types': 'application/json' } }).catch(error => { this.saveMessagesInOffline(messages); throw error; }); } ... }
class MessagesService {
    async createMessages(messages) {
        retur fetch('https://api/messages', {
          method: 'POST',
          body: JSON.stringify({
              content: messages.content,
              author_id: messages.author_id
          }),
          headers: {
              'Content-Types': 'application/json'
          }
        }).catch(error => {
            this.saveMessagesInOffline(messages);
            throw error;
        });
    }
...
}

The fetch() will reject Promise only when there are some connection problems, and hence catch() will handle it. On the off chance, if that occurs, we can store our messages that Background Sync will sync. To do so, let’s add one more method inside this class in the subsequent step.

Storing messages in IndexedDB

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
async saveMessagesInOffline ( messages ) {
const db = await openDB ( 'messages' , 1 , {
upgrade ( db ) {
db. createObjectStore ( 'messageToSync' , { keyPath: 'id' }) ;
}
}) ;
const tx = db. transaction ( 'messagesToSync' , 'readwrite' ) ;
tx. store . put ({ ...messages }) ;
await tx. done ;
}
...
... async saveMessagesInOffline(messages) { const db = await openDB('messages', 1, { upgrade(db) { db.createObjectStore('messageToSync', { keyPath: 'id' }); } }); const tx = db.transaction('messagesToSync', 'readwrite'); tx.store.put({...messages}); await tx.done; } ...
...
async saveMessagesInOffline(messages) {
    const db = await openDB('messages', 1, {
        upgrade(db) {
            db.createObjectStore('messageToSync', { keyPath: 'id' });
        }
    });
    const tx = db.transaction('messagesToSync', 'readwrite');
    tx.store.put({...messages});
    await tx.done;
}
...

In the context of offline support, the method looks like as above:

  • saveMessagesInOffline(messages) is an asynchronous function. It stores the messages in the indexedDB so that the Service Worker can have access while performing the Background Sync.
  • openDB(‘messages’, 1, { upgrade(DB){} }); is used to open the database of indexedDB and return that as an object. The params in the method are the database name (messages), the version (1), and the object itself. The upgrade function is called only when openDB hadn’t opened such a version of the selected database previously.
  • db.createObjectStore(‘messageToSync’, { keyPath: ‘id’ }); this method is used to create the store for caching messages that you’d wish to sync, and where:
    • “messagesToSync” is the name of your store.
    • { keyPath: ‘id’} are options of the store that set id as the key path.
  • db.transaction(‘messagesToSync’, ‘readwrite’); passing the store name “messageToSync” to give the access in read-write mode.
  • tx.store.put({…messages}); The put() updates the given object in the store, or inserts a new object if it does not exist.
  • tx.done; finishes the transaction, you need to use transactionObject.done().

Now, your messages are ready to sync.

Sync messages

The sync event in the service worker file will utilize the syncMessages() inside the listener. We will open the database using the openDB(); and will iterate all the messages using getAll() and will call the fetch() to send messages through the given API.

It will be synced from the indexedDB store and sent to the API. Once all the messages are sent successfully, we will delete the messages from the store using the message-id.

The app now incorporates background message syncs!

About the author

Gargi Sharma

Gargi is a software engineer with Excellarate for close to 3 years and is part of the UX team. She has substantial interest and experience in Progressive Web Application, React JS, JavaScript and Java. Gargi has a Masters in Computer Application from Pune University.

Share this post