Progressive Web Apps (PWAs) are revolutionizing the mobile web by merging the best aspects of both websites and native mobile applications. Imagine a website built with familiar web technologies that behaves and feels like a native app. This guide will introduce you to the world of PWAs, exploring how they leverage recent browser advancements to deliver an enhanced user experience.
PWAs, initially proposed by Google in 2015, have rapidly gained traction due to their simplified development process and the immediate improvements they offer to the user experience. They empower web developers to create installable web apps, enable push notifications, and even provide offline functionality, all while leveraging the existing web ecosystem.
Why Choose Progressive Web Apps?
Traditional native apps often face significant user drop-off at each step of the installation process. Studies show that apps can lose up to 20% of their users between initial contact and actual usage due to the friction of finding, downloading, and installing. PWAs bypass these hurdles, allowing users to instantly access the app and start engaging without the need for installation. Users can then be prompted to install the app for a full-screen experience upon returning.
While native apps excel in areas like push notifications (leading to up to three times higher retention rates) and performance (due to local resource storage), PWAs bridge the gap by offering improved retention and performance without the complexity of managing a native application.
Use Cases for PWAs
PWAs are particularly well-suited for applications that users are expected to access frequently, mirroring the typical use case for native apps. E-commerce platforms like Flipkart have successfully implemented PWAs (Flipkart Lite) to deliver a seamless experience. Similarly, SBB utilizes a PWA for online check-in, enabling users to access their tickets offline.
When determining whether to build a PWA, a traditional website, or a native mobile application, consider your target audience and their primary actions. PWAs are inherently “progressive,” functioning across all browsers and adapting to leverage enhanced features as browsers evolve.
If your website already has an application-like interface, embracing PWA principles will undoubtedly elevate the user experience. However, if critical features lack cross-browser support, a native app might be necessary to ensure consistent functionality for all users.
Key Characteristics of a Progressive Web App
To truly understand PWAs, it’s crucial to grasp their defining characteristics:
- Progressive: PWAs function on any device and enhance functionality progressively, taking advantage of the features available on the user’s specific device and browser.
- Discoverable: As websites, PWAs are readily discoverable by search engines, a significant advantage over native applications.
- Linkable: PWAs leverage URIs to represent the application’s current state, allowing users to bookmark and share specific states within the app.
- Responsive: The PWA’s user interface adapts seamlessly to different device form factors and screen sizes.
- App-like: PWAs should mimic the look and feel of native apps, employing an application shell model to minimize page refreshes.
- Connectivity-Independent: PWAs are designed to function reliably even in low-connectivity environments or offline. This is often achieved via service workers.
- Re-engageable: PWAs utilize features like push notifications to encourage users to return to the app, similar to native mobile applications.
- Installable: PWAs can be installed on a device’s home screen, providing quick and easy access.
- Fresh: PWAs automatically update content when the user is connected to the internet, ensuring they always have the latest information.
- Safe: PWAs must be served over HTTPS to prevent man-in-the-middle attacks, especially since service workers can intercept all network requests.
Diving into Code: A Simple PWA Example
Let’s explore a basic PWA example: “Sky High,” an airport arrival schedule simulator. This app will display a list of upcoming flights fetched from an API. When offline, the app will show the last downloaded flight schedule.
The Fundamentals
The core principle of a PWA is to work on all devices while enhancing the experience on devices and browsers that support advanced features. We’ll construct our website using standard HTML5 and JavaScript to simulate data retrieval from a mock API.
Our website adheres to Google’s material design guidelines, ensuring a consistent and visually appealing user experience across devices. We’ve also optimized the app to be jank-free, aiming for a smooth 60 frames per second rendering to improve user engagement.
For demonstration purposes, we’ll use a static JSON file instead of a real API. In a production environment, you would typically interact with an API or use WebSockets.
index.html
<title>Sky-High Airport Arrivals</title> <div> <div id="main"> <ul data-bind="foreach: arrivals"> </ul> </div> </div>
This index.html
file is a standard HTML structure. We utilize Knockout via the data-bind="foreach: arrivals"
attribute to bind the arrivals
property of our ViewModel to an HTML list. For each flight in the arrivals
array, the title
, status
, and time
properties are bound to the corresponding HTML elements.
page.js
(function() { // declare the view model used within the page function ViewModel() { var self = this; self.arrivals = ko.observableArray([]); } // expose the view model through the Page module return { vm: new ViewModel(), hideOfflineWarning: function() { // enable the live data document.querySelector(".arrivals-list").classList.remove('loading') // remove the offline message document.getElementById("offline").remove(); // load the live data }, showOfflineWarning: function() { // disable the live data document.querySelector(".arrivals-list").classList.add('loading') // load html template informing the user they are offline var request = new XMLHttpRequest(); request.open('GET', './offline.html', true); request.onload = function() { if (request.status === 200) { // success // create offline element with HTML loaded from offline.html template var offlineMessageElement = document.createElement("div"); offlineMessageElement.setAttribute("id", "offline"); offlineMessageElement.innerHTML = request.responseText; document.getElementById("main").appendChild(offlineMessageElement); } else { // error retrieving file console.warn('Error retrieving offline.html'); } }; request.onerror = function() { // network errors console.error('Connection error'); }; request.send(); } } })();
The page.js
file exposes the Page
module, containing the vm
(ViewModel) and functions to manage offline warnings. The ViewModel
is a simple JavaScript object with an arrivals
property, an observable array that dynamically updates the HTML when data changes.
The hideOfflineWarning
and showOfflineWarning
functions manage the display of an offline message. showOfflineWarning
adds a loading
class and retrieves the offline.html
file via XHR to display a custom offline message.
Web App Manifest: Making it App-Like
To enhance the app-like experience, we use a web app manifest file, a JSON file adhering to the W3C specification. This manifest enables features like full-screen mode, custom icons, and theme colors. Chrome on Android utilizes this manifest to proactively suggest installation via a web app install banner.
To trigger the installation prompt, your web app must:
- Have a valid web app manifest file.
- Be served over HTTPS.
- Have a registered service worker.
- Have been visited at least twice with five minutes between visits.
manifest.json
{
"short_name": "Arrivals",
"name": "Arrivals at Sky High",
"description": "Progressive web application demonstration",
"icons": [
{
"src": "launcher-icon.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "launcher-icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "launcher-icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "launcher-icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "launcher-icon-256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"start_url": "./?utm_source=web_app_manifest",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#29BDBB",
"background_color": "#29BDBB"
}
Let’s break down the key elements of the manifest file:
"short_name"
: A concise name for the application (used on the home screen)."name"
: The full human-readable name of the application."description"
: A general description of the web application."icons"
: An array of image objects that represent the application’s icon in various sizes. These icons are used on the splash screen, home screen, and task switcher."start_url"
: The URL that loads when the app is launched. Theutm_source
parameter is used for tracking installations."display"
: Specifies the app’s display mode:"fullscreen"
,"standalone"
,"minimal-ui"
, or"browser"
."standalone"
provides the most app-like experience, removing browser UI elements."orientation"
: Sets the default screen orientation:"portrait"
or"landscape"
."theme_color"
: Sets the default theme color for the application, often used to color the status bar on Android."background_color"
: Defines the background color for the web application, used as the splash screen background color in Chrome.
Include a reference to manifest.json
in the <head>
section of your index.html
file:
Once installed, users can quickly access the app directly from their device, enhancing engagement.
Service Workers: Enabling Offline Functionality
One of the most exciting features of PWAs is their ability to function offline. Service workers allow you to display previously cached data (using IndexedDB) or show a custom offline page.
Service workers are event-driven JavaScript scripts that intercept network requests, enabling caching of static resources to significantly reduce network usage and improve performance.
Application Shell Architecture
The application shell is the minimum HTML, CSS, and JavaScript required to render the user interface. Native apps bundle the application shell within their distributable. PWAs, however, cache the application shell’s resources in the browser. In our “Sky High” app, the application shell comprises the header bar, fonts, and associated CSS.
To implement service workers, create a sw.js
file in the root directory.
sw.js
// Use a cacheName for cache versioning
var cacheName = 'v1:static';
// During the installation phase, you'll usually want to cache static assets.
self.addEventListener('install', function(e) {
// Once the service worker is installed, go ahead and fetch the resources to make this work offline.
e.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll([
'./',
'./css/style.css',
'./js/build/script.min.js',
'./js/build/vendor.min.js',
'./css/fonts/roboto.woff',
'./offline.html'
]).then(function() {
self.skipWaiting();
});
})
);
});
// when the browser fetches a URL…
self.addEventListener('fetch', function(event) {
// … either respond with the cached object or go ahead and fetch the actual URL
event.respondWith(
caches.match(event.request).then(function(response) {
if (response) {
// retrieve from cache
return response;
}
// fetch as normal
return fetch(event.request);
})
);
});
Here’s a breakdown of the service worker code:
cacheName
: A variable to manage cache versioning. Changes to cached assets should increment this version.install
event listener: Fires during the service worker’s installation phase. This is where we define the assets to be cached usingcache.addAll()
.self.skipWaiting()
forces the waiting service worker to become active immediately.fetch
event listener: Intercepts network requests. It first checks if the requested resource is in the cache usingcaches.match()
. If found, the cached resource is returned. Otherwise, the request is fetched from the network.
Register the service worker in your JavaScript (main.js
in this example):
// Register the service worker if available.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').then(function(reg) {
console.log('Successfully registered service worker', reg);
}).catch(function(err) {
console.warn('Error whilst registering service worker', err);
});
}
window.addEventListener('online', function(e) {
// Resync data with server.
console.log("You are online");
Page.hideOfflineWarning();
Arrivals.loadData();
}, false);
window.addEventListener('offline', function(e) {
// Queue up events for server.
console.log("You are offline");
Page.showOfflineWarning();
}, false);
// Check if the user is connected.
if (navigator.onLine) {
Arrivals.loadData();
} else {
// Show offline message
Page.showOfflineWarning();
}
// Set Knockout view model bindings.
ko.applyBindings(Page.vm);
This code snippet registers the service worker and includes event listeners to detect changes in the network connection status. When the app goes offline, Page.showOfflineWarning()
is called to display the offline message. When the app comes back online, Page.hideOfflineWarning()
is called, and the data is reloaded. The code also initializes the Knockout bindings.
After the initial load, subsequent reloads will retrieve resources from the service worker, demonstrating the application shell in action.
Offline Testing
Users accessing the application without an internet connection will see the application shell and the custom offline warning, providing a much better user experience than the browser’s default offline page.
Performance Considerations
Service workers offer significant performance gains through caching. By caching static assets, PWAs can dramatically reduce page load times, particularly on slow networks.
Google Lighthouse: Auditing Your PWA
Lighthouse is a powerful tool for auditing PWAs. It’s available as a Node.js module or a Chrome extension. Lighthouse evaluates your website against PWA best practices, providing a report on areas for improvement, such as the presence of a manifest.json
file and offline availability.
To install Lighthouse globally:
npm install -g lighthouse
To run Lighthouse against a live website:
lighthouse https://example.com
Push Notifications: Re-engaging Users
Push notifications allow PWAs to re-engage with users by delivering timely updates even when the browser is closed.
Conclusion
PWAs represent a significant advancement in web development, offering a compelling blend of the best features of websites and native apps. This guide has provided a foundational understanding of PWAs, covering their key characteristics, implementation details, and benefits.
Explore further possibilities like push notifications using the Push API, enhancing re-engagement, and utilizing IndexedDB and background syncing for improved offline experiences.
Cross-Browser Compatibility
While PWA support is growing, cross-browser compatibility remains a consideration, especially for Safari and Edge. However, Microsoft actively supports PWAs and is expected to implement more features.
- Service workers and Cache API: Supported in Chrome, Firefox, Opera, and Samsung Browser. Under development in Microsoft Edge and under consideration for Safari.
- Add to home screen: Supported in Chrome, Firefox, Opera, Android Browser, and Samsung Browser. Microsoft plans to list PWAs in its store. No plans for Safari.
- Push API: Supported in Chrome, Firefox, Opera, and Samsung Browser. Under development in Microsoft Edge. No plans for Safari.
Increased adoption of PWA features will encourage broader browser support, ultimately benefiting both developers and users.