An In-Depth Overview of Progressive Web Applications
If you are a web developer, you are probably aware of progressive web applications (PWAs) or have implemented your own already. If you are unfamiliar, here is an overview of what progressive web applications are and why they are important in modern web development.
A progressive web app is defined as a responsive, connectivity-independent, app-like, fresh, safe, discoverable, re-engageable, installable, and linkable web experience. That is a lengthy description. How about a simpler one? Well, I'm afraid this is as simple as it gets. I would argue that a PWA is just a web application with a GREAT user experience, but that's not so descriptive either.
To understand this definition in more detail, let’s examine each of the core principles. You may be familiar with some of them already, and others may catch you by surprise or seem ambiguous.
Principles of progressive web apps
- Responsive
- Connectivity-independent
- App-like
- Fresh
- Safe
- Discoverable
- Re-engageable
- Installable
- Linkable
Responsive
This can mean two things depending on your interpretation. To web developers, it means that the elements displayed in the application are scaled appropriately on different screen sizes. To other developers, this may relate to performance, usually indicating that the application responds quickly to user interactions, events, page loads, etc. Building a performant application is important, and progressive web apps do this naturally. However, this is not what we are addressing, so we'll follow the first definition. Responsive “web” apps look nice on any device. Whether the screen is wide, narrow, short or tall, the application layout scales and adjusts to the user’s screen appropriately.
Connectivity-independent
Since we are talking about web applications, a network connection is needed at some point in the applications lifecycle, especially when the application is visited for the first time. But this is not always possible. When the network is not available or too slow, the application should still work on a device. Navigating through the application should not reveal blank pages or 400 errors to the user. Utilizing the browser’s storage mechanisms makes this possible.
App-like
I think "native app-like" is a better way to describe this principle. Native applications look and feel like they were made for the device that they are used on. Some applications are built specifically for a platform like iOS or Android, and the web counterpart doesn’t provide the same experience, especially on mobile. Progressive web applications aim to provide similar experiences across ALL devices. Users can switch from their phone to a laptop and accomplish the same tasks easily.
Fresh
I like to refer to this as A.F.A.P. -- the data in your application should be As Fresh As Possible. If new data is available and relevant to the client, then the application needs to be updated with the freshest data. Managing network requests and browser storage is essential to providing a great user experience and keeping content up to date on the client.
Safe
Safety first! What good is an application if it is not safe to use? Most consumer web applications contain sensitive information that should only be known to the communicating parties. Protecting the data shared in the application is necessary. To keep it simple, use the HTTPS protocol to add a layer of security to the network traffic.
Discoverable
Discoverability in relation to progressive web apps is important. The application should be easy to find on the web. If search engines can’t find the application, then how will the potential users? Applying search engine optimization techniques can help. The application manifest also plays an important role in identifying the application to the browser. It contains information about the application including the name, author, and description. The manifest also helps identify a PWA to the device it is installed on. Yes, PWAs are installable (this is also another principle).
Re-engageable
Re-engageable applications can pull the user back by sending push notifications. It’s a way to let users know that something interesting has happened that requires their attention. We are used to this notion from our smartphones and native mobile applications, but browsers support this also via the Push and Notification APIs.
Installable
Progressive web applications can be installed directly to the home screen on a mobile device. This is primarily a mobile browser feature, but with Chrome, you can do this on your desktop too. This is also supported in iOS Safari, so if you have iPhone, you can join the PWA craze. What is really cool about installing web applications is you don't need to go through a marketplace like the App Store or Google Play Store to download an app. Just visit the site, click “Add to Home Screen” option, and the app will appear on your home screen immediately.
Linkable
A linkable web application is a shareable one, so applications hosted on private domains do not apply. All you need is a URL.
Creating a progressive web app
So how does all of this translate in practice? In comparison to a standard web application, there are only three additional requirements to create a PWA:
- Serve web app over HTTPS
- Add an Application Manifest file
- Use a service worker
HTTPS is the de facto protocol for modern web applications, and a progressive web application is no exception.
The application manifest is a JSON file that contains metadata about the application. It only provides basic information. In an Android application, the manifest file is much more complex and may need to be changed throughout the development of the app. Web app manifests are less involved and don’t need to be updated often after creation because they do not contain configuration parameters or dependencies.
The last step in creating a progressive web application is to add a service worker. This is where the magic happens and offline capabilities are enabled. The service worker is just another JavaScript file -- a very powerful JavaScript file. It actually runs on a separate thread in the browser, so executions on the service worker thread won’t interrupt the main application thread. This gives the developer flexibility to create a better user experience via parallelism. The service worker can handle network requests/responses and caching. Removing this work from the main thread separates the application logic from the data management and network related operations.
As you can see, most of the progressive work here involves implementing the service worker. But before implementation, the architecture of the application must be considered.
PWA Application Structure
The application shell is a concept that describes the infrastructure of the application. It consists of all the static files that your app needs to function. In the context of web development, this would include HTML, CSS, JavaScript, and image files.
The Content is data that can change throughout the lifecycle of the application. It is excluded from the application shell because it is dynamic and could potentially be stale when the app is loaded. It is typically exposed via an API service and easy to query. This content needs to be managed in the application to ensure the freshest content is available upon request. The service worker assumes this responsibility.
On first load of the application, the application shell files should be cached, so the application can work without a network connection.
A good PWA will avoid ever displaying this screen:
When a page does not load, the user becomes totally disengaged with the application. Obviously, network-related issues impact the user experience, but it should not push users away from the app. The idea is to resemble a native experience, and a PWA can keep users engaged in the application even if a blank screen is displayed in the app. To keep users engaged when the network is slow, you can use animations or provide client-side interactions that give some visual feedback. It could be as simple as a refresh button with a spinner, a little puzzle, or a 3D interactive model. Be creative!
One drawback of using the application shell model is its performance. It does slow down the initial load time; however, this can be improved. To cut down on the time it takes to load app shell files you can try minifying code (to reduce file sizes), bundle files (to minimize the number of network requests), or remove code that isn’t used. You can send this removed code to the client when it's needed. This will vary based on requirements.
The architecture described here is very common. If you have developed applications for other platforms, you may recognize the similar design structure. For example, mobile applications that require access to the network follow a similar approach to communicate with services. There are typically some Factory classes that handle network requests and responses. The Factory class provides a layer of abstraction and works best if spawned asynchronously. The application logic does not have to wait on the request. It can allow the user to move on and notify them when the request is fulfilled. This also facilitates easy testing by separating data access utilities and UI logic.
Using the App Shell model is a good place to start, but it is not a requirement for a progressive web app. If you have an existing application, you can assess what portions of your application are used the most and optimize the initial loading. If 95% of your user base only use 25% of your app, it may make sense to only download and cache the 25% of the application (that is most actively used). The other portions can be downloaded and cached as needed. It all depends on how users interact with your application.
Service worker
Service worker implementations will vary from one application to another, but there are a few things you should know about to begin using them in your apps. Earlier, I mentioned that the service worker runs on a different thread in the browser. This means that it has a lifecycle to manage, just like your application.
Here are the main service worker lifecycle events:
1. Register
The registration of the service worker will occur when the application is loaded in the browser for the very first time. The isn’t really a Service Worker event, since the service worker doesn’t exist in the browser context at this moment in time. But it is an important step. The main JavaScript file of the application should check that the ServiceWorker API is supported in the browser, and if so, register the service worker. Upon successful registration, the service worker file is downloaded, and thereafter the install begins.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js');
}
This code registers the service worker in the browser (if supported). The next events are all handled in the service worker file.
2. Install
The Install event is the first event that the service worker can handle itself. It starts immediately after the registration/download. Upon completion of the install, it is a good idea to begin caching static assets, since the install event only occurs once.
self.addEventListener('install', function(e) {
e.waitUntil( // waitUntil() from ExtendableEvent
caches.open(cacheName).then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
);
});
The waitUntil() method takes a promise that will execute after the install event has completed.
3. Activate
The activate event means the service worker is installed. Once activation is completed, the service worker gains control of the main application. When a service becomes "active," it should also check the cached resources, and update the data if it is stale. This may require making additional network requests to make comparisons, but that shouldn’t be a problem since the application will not be affected by the execution of the request. The service worker is also capable of performing operations on functional events like Fetch, Push and Message when it is in an active state.
Note: After a service worker is registered and installed, it will exist in the browser until the user deletes it. The file is not automatically removed when the user closes out of the application, and the browser will download the service worker file every 24 hours to avoid bad/stale code.
4. Fetch
The Fetch event is triggered whenever a network request from the main application is invoked. When this happens, the service worker becomes responsible for the request. If the information requested is already cached, the service worker can return that information and bypass the network altogether. Or it can still send the request, compare the response with the cached info, and update if necessary. Choose a strategy that works best for the users.
The Push and Message events are also events that the service worker listens for when active. They can be used for to implement push notifications and synchronize data being sent out.
As you can see, the service worker is where the majority of the work is done and an essential piece to making a web app progressive. It acts a network proxy and storage management service for your application and is a great tool to improve user- experience in web applications.
Building a progressive web app
Try building a PWA. It's easy to get started if you already have a web application. We are currently working on an article describing how we built a PWA with Wijmo. It is an example of how to migrate an existing application to progressive standards.
If you like the idea of progressive web applications, but you are still not sure if they are right for your business, check out 7 Reasons your Company Needs Progressive Web Apps.
If you have questions or comments, please share below!