Back to the homepage
Angular

Progressive Angular part 1

You’ve probably experienced many times the situation when, despite having access to the network, a website couldn’t load and you were staring endlessly at the white screen. It’s one of those experiences where your patience runs out with every second, but you still hope that something will appear on the screen after a while.

This all comes from the fact that we assume the user always has a good quality internet connection, Online first (or Network-first). To give the user the best possible experience, we should change our approach and use Offline first (or Cache-first), which means that we throw network availability into the background and assume that our application is working offline.

For this purpose, we can use Progressive Web Apps. On our blog, in the first part, we will touch on the very concept of this technology, and in the next part, we will present how we can transform our existing Angular application into a PWA.

  1. Progressive Web App
    1.1. PWA vs Native vs Hybrid
  2. Service Worker
    2.1. Scope
    2.2. Registration
    2.3. Life cycle
    2.4. Events
    2.5. Communication
    2.6. Precaching
    2.7. Versioning
    2.8. Caching strategies
    2.9. Multiple Service Workers
    2.10. Recipes
    2.11. Angular Service Worker
    2.12. WorkBox
  3. Summary

 

Progressive Web App

These are applications that are installed from the level of a web browser when visiting a website which supports this technology. PWAs are available on both desktop and mobile platforms but were developed mainly for mobile platforms.

While visiting an app page with PWA support, it is possible to add such to our device. Then it is available on our device as native.

One of the basic principles of the PWA was to operate without access to the network. This is possible by using so-called Service Workers, which mediate communication between the browser and the Internet. This allows strategies such as stale-while-revalidate to be implemented, providing the user with access to the application and its cached resources while synchronizing with the network in the background.

An illustrative diagram of a PWA application.

PWA vs Native vs Hybrid

PWA applications, unlike native or hybrid applications, must have access to the web to function properly, after all, they are built on top of existing websites.

Access to native functionalitiesis relatively limited, the number of features available continues to grow but ultimately diverges from hybrid or native apps (currently supported features can be found on What Web Can Do Today).

PWA, on the other hand, allows installation on any platform that has a browser with appropriate support for Service Workers. An additional advantage is that our application doesn’t have to be published in the shop of a given platform.

Also worth mentioning is the relatively low performance of these types of applications compared to native ones and the danger of limited security features. A comprehensive comparison of native, PWA, and hybrid applications in selected categories is shown in the graphic below.

A breakdown of native, PWA, and hybrid apps in selected categories.

Service Worker

It is a mechanism that acts as a kind of proxy between the application, the web browser, and the network. It enables, among other things, caching of application resources, synchronization of data in the background or calling Push Notifications. These functionalities are made possible, among other things, by intercepting outgoing requests.

Unlike other application scripts, the Service Worker is preserved when a tab or browser is closed. When the application starts up again, the SW is loaded first and can intercept any requests to the application’s resources. Properly implemented, SW can completely load the application without access to the network.

Service Workers are completely asynchronous and run in an individual thread that doesn’t block rendering. Because of this, they do not have access to the DOM tree and other synchronous functionalities and libraries such as local storage (but they do have access to IndexedDb) or XHR.

Additionally, for security reasons, they can only be used with HTTPS (outside of a local environment where localhost is available). A full list of available features can be found on the HTML5 worker test page.

Scope

Within a single application, many different SWs can operate, being responsible for different scopes of the application. The range in which an SW operates is called a scope.

Within a given scope, only one Service Worker can be registered and active.

In simple terms, the scope of an SW is all files and directories located “underneath” in the application tree, i.e. an SW defined at the level of our main module controls the whole application.

It is possible to set the scope of a given SW during its registration, limiting its operational capabilities to a certain pool of addresses. For example, by setting the scope to /api/, our SW will be able to intercept requests to /api/ or /api/image, but will not control addresses higher in the hierarchy, such as /api (without the final slash) or /.

For Angular Service Worker, you need to pass an additional configuration object.

Registration

During the first visit to our application, the Service Worker is installed and immediately activated.

Next time you visit, when the browser detects that a new version of SW is available, then it is installed but not yet activated. Then we say that it is a worker in waiting, a Service Worker waiting for its turn.

Activation of a waiting SW occurs when none of the parties controlled by the previous, old SW are no longer loaded.

However, there may be a situation when we want to activate the updated SW immediately. In this case, we can use ServiceWorkerGlobalScope.skipWaiting(), the function that activates our new SW along with Clients.claim(), the method that transfers all current “clients” under the control of the new SW.

From now on, all outgoing requests and additional functionalities will be controlled by the updated Service Worker.

Life cycle

Each Service Worker has a specific life cycle. In short, it comes down to registration of the selected SW, installation and activation. If an error occurs during any of these steps, the selected SW isn’t registered or is replaced by another.

By listening for selected events, we can adapt the steps performed during initialization to our requirements, for example to cache resources.

Diagram illustrating the various stages of Service Worker registration.

Events

Using the Service Worker we have the possibility to connect to various types of events. Some of them are related to the life cycle (Lifecycle Events), which gives us the ability to perform initializing operations at a particular moment. Others, on the other hand, are related to various functionalities that SW gives us (Functional Events).

The most commonly used events include:

  • install – emitted after the initial parsing of SW, during its installation; this is where pre-caching usually occurs, more discussed in the Precaching chapter. At this point, we can also skip the waiting stage using the ServiceWorkerGlobalScope.skipWaiting() function mentioned earlier.
  • activate – the emission occurs after the installation; here we can complete the initialization operations, perform cleanup after the previous SW,
  • message – event connected with communication between SW and application using PostMessage API, described in more detail in the next chapter,
  • fetch – event related to all outgoing requests from the application, which gives us the possibility to implement various caching strategies; SW acts here as an interceptor.

For a full list of supported events please refer to the W3C draft. There are, among others, events that allow you to perform background synchronization (Background Synchronization API) or use Push Notifications.

Communication

As we mentioned earlier while discussing the available events, communication between the application and the Service Worker takes place using the PostMessage API.

In practice, it comes down to sending and listening for messages passed along with the message event.

In the case of SW, the message is sent by first obtaining a Client instance and then by calling the Client.postMessage method. We can do this by calling the Clients.matchAll() method or Clients.get() if we have access to the Client ID.

On the application side, communication is carried out using the ServiceWorkerContainer instance available in the global Navigator.serviceworker object. Then, we have access to the postMessage method responsible for sending messages to SW and to the propertion ServiceWorkerContainer.onmessage, which is also the function called when a message arrives.

We will use this mechanism in the next part of the article dedicated to integration,to pass information to the client about a new, available graphic.

Precaching

Application sources and resources can be divided into static and dynamic, modified during its operation. To the first, we can include, for example, obfuscated files containing the application code, styles, or assets. Variable resources, on the other hand, include mainly cached responses from the server, graphics.

Having this distinction in mind, saving constant application resources to cache can be done already during SW installation. This approach is called PreCaching, because this operation is performed before SW activation.

You can also meet the term cache warming, which says about moving part of the caching logic during the application (runtime) to a method called during SW installation.

Versioning

There are only two hard things in Computer Science: cache invalidation and naming things.

– Phil Karlton.

 

The idea of caching an application’s resources to optimize its performance is perfectly valid, but unfortunately, as is usually the case, it is problematic to maintain.

In the case of PWA, the most troublesome case is when a new version of SW appears. Then, a newer SW may send requests for new data, which will still be served by the currently active one, which may lead to unpredictable behavior.

The solution to the above problem is to version the cache based on the current version of the application. As when running the application, the previous SW will still pull the updated sources into the old cache, when activating (activate event) our new SW, we need to take care of clearing the data from the previous cache.

Detailed information with examples can be found in the MDN documentation.

Caching strategies

When deciding to use SW, we will certainly want to take advantage of data caching. Depending on the needs of the application, there are several main strategies:

  1. Stale-while-revalidate – this strategy allows data to be served from the cache, performing background synchronization with the server in the meantime to update the data. This is the most commonly used approach for applications where data timeliness is not a priority.
  2. Network first (online first) – this approach consists of first trying to fetch data from a remote source, and in case this fails, using data from the cache.
  3. Cache first (offline first) – in contrast to the above strategy, the data currently contained in the cache are retrieved first. If it is not available, we retrieve it from the network.

More information about this can be found in the WorkBox documentation. For more advanced strategies we refer you to Jake Archibald’s blog.

Multiple Service Workers

When writing our application, we may want to use multiple SWs within a common scope. It is also possible that several SWs will listen for the same event. Recalling the paragraph about the scope, we know that within a given scope only one Service Worker can operate.

In the case of registration of multiple SWs, the one that registers later will actually function, which rather does not solve our problem. What should we do in this case?

With help comes the WorkerGlobalScope.importScripts() method, which allows us to import another SW definition. In short, this solution is about merging two or more implementations into a single SW.

In the case of several methods listening for the same event, the order in which they are executed depends on the location of importScripts() and the definitions of these methods themselves. The principle is simple and it is that the logic of simply predefined handlers is executed first.

Additionally, in the case of outgoing request interception (fetch), subsequent methods listening for a given event will be called only if the previous one did not end with a call to FetchEvent.respondWith().

It should also be mentioned that resources imported using importScripts() are cached automatically.

In the next integration section of this article, we use this mechanism as part of extending Angular Service Worker to communicate with the application about the availability of new data.

Recipes

The implementation of SW was unfortunately problematic sometimes and contained a lot of boilerplate code. As the functions performed by SW are common to most PWA applications, Mozilla and other contributors have created a set of “recipes” on how to implement certain functionalities, available at serviceworke.rs. This is also great API documentation, making it much easier to understand what a piece of code is responsible for.

Angular Service Worker

For Angular applications, PWA support is implemented using the @angular/service-worker package. It provides a dedicated module which we use to configure our application.

In addition, it contains services that package events emitted by SW to enable operations related to updates (SwUpdate) and the use of Push Notifications using the SwPush service.

This package defines SW as containing the implementation of severalmechanisms such as cache, Push Notifications, background sync. This means, among other things, that it is possible to implement Offline-first, Online-first, and stale-while-revalidate strategies.

Which of these functionalities our application will use depends on the configuration in the ngsw-config.json file. In short, it defines the behavior of our application when fetching different resources. A detailed discussion of available options can be found in Service Worker Config documentation.

An additional advantage of using NGSW is its integration with the Angular application, and more specifically we have the ability to configure the moment of SW registration, thus not interfering with the operation of our application (more about this will be said in the next part of the article).

The presented solution of using a ready SW, which can be configured to your needs is great in the case when we want to use basic functionalities.

Problems start when our application requires something non-standard. Then it is possible to “extend” Angular SW with additional functionalities. This process will be described in detail in the next part of the article.

WorkBox

An alternative approach is to use the WorkBox library. This is a set of ready-made packages specialized for specific purposes, developing by Google engineers. The full list of available modules can be found on the official WorkBox website.

The use boils down to the implementation of SW using these packages, with the appropriate configuration that meets our requirements. An example use will be presented in the next part of the article.

Summary

Progressive applications are an interesting proposition when it comes to supporting mobile or desktop platforms. By properly using the available functionalities, we can significantly improve the User Experience of our application.

However, we should keep in mind that such applications still differ from native applications in many aspects, being more a kind of alternative to hybrid products (check PWA vs Native vs Hybrid).

Nowadays, every browser and most platforms have support for PWA, so we think this is a proposition definitely worth considering.

We will soon publish the next part of the article, in which we will focus on the practical use of PWA and present the integration process of a sample Angular application. Stay patient!

About the author

Marcin Leśniczek

Marcin is constantly hungry for knowledge and passionate about mobile and hybrid applications. Always open to new ideas and technologies, being nit-picky. He dedicates his free time to science and astronomy.

Don’t miss anything! Subscribe to our newsletter. Stay up-to-date with the latest trends, tips, meetups, courses and be a part of a thriving community. The job market appreciates community members.

Leave a Reply

Your email address will not be published. Required fields are marked *