I believe Progressive Web Apps (PWAs) are the most impactful innovation in web development since CSS media queries. Pushed ahead by Google, they are likely to replace native mobile apps little by little, just as media queries made mobile sites obsolete. In this article, I’d like to describe a PWA we developed in 2019, the principles we followed during its development and dissipate some of the confusion around what makes a progressive web app and what doesn’t.
Background to our PWA project
In 2016 we wrote a mobile app for CBM using Angular 2 and Ionic. This is known as a hybrid approach, where the app is coded in JavaScript which is then compiled to a native app. The app was well thought out and users were very satisfied with it. But unfortunately the technology behind it let us down. Libraries and frameworks fell out of support. People who originally developed the app left and no-one else in the studio could maintain it – the technologies were too niche. We are not a mobile app development agency. What we do best is PHP-based web apps.
Then came Progressive Web Apps. These are web applications that have features of native mobile apps. For example, navigating content whilst offline, ‘installing’ the app (adding its icon to the homescreen for direct access), push notifications.
At their core, they are web applications, based on technologies we are very familiar with (PHP, HTML, CSS, JavaScript), that have an added JavaScript layer to add native-like capabilities to them. The ‘Progressive’ in ‘Progressive web application’ comes refers to the concept of ‘progressive enhancement’. Strip the PWA scripts, and you are left with a web app that works fine (provided you made the right application design decisions). The PWA ‘layer’ is an added benefit that kicks in the most recent browsers.
We therefore decided to re-build the CBM app as a Progressive Web App. (Actually, we developed a second app using the same codebase).
What the CBM PWAs do
The CBM HHoT and i-DRR apps provide resources for humanitarian workers on the field in the form of information cards. Essentially, they are libraries of information cards or pages. For example, a disaster relief worker needs to build a ramp to access a temporary building using a wheelchair. He/she can navigate the app to find the ‘ramp’ card, which describes how to build the ramp and to which dimensions and inclination.
Using PWA techniques, the app user can download the whole site data (cards and pages) and then navigate the whole app when offline, as you would after installing a mobile app.
Even if the user decides not to download the data, visited pages are cached in the browser cacheStorage, a new, persistent cache we can explicitly add and remove content to (we do not have that level of control over the other ‘traditional’ browser cache). Caching pages and other resources such as images greatly improves performance. Visited pages load fast and can still be viewed when offline.
Users can also perform a keyword search to find a card and mark cards as ‘favourites’. They can also add the app to their homescreen.
More technical details
The websites follows a headless architecture. The content of the apps is managed via a Drupal 8 CMS. We extended the Drupal API to serve the content to a front-end application.
The front-end application is a PHP Symfony web application, using the usual MVC pattern. It receives requests, queries the corresponding data via the Drupal API mentioned above and serves the right view populated with the content.
And on top of that, there’s the JavaScript application, some of which uses PWA technologies, e.g. a serviceWorker and the cacheStorage. The application does a series of things.
Some are available to a wide range of browsers (even those that don’t support PWA technologies):
- Users can ‘favourite’ cards. These will be listed in the localStorage of the browser.
- Users can download the cards data into an IndexedDB database.
- They can then search for cards via a keyword search.
- Users can switch between three languages.
Other features are only available in the latest browsers, those that support serviceWorkers and cacheStorage:
- Website assets (CSS, JS scripts, HTML templates, template images) are stored in the cacheStorage on installing the serviceWorker.
- Visited pages are cached in the cacheStorage by the serviceWorker.
- When downloading the site data, images are also stored in the cacheStorage in advance.
- The site can be browsed when offline.
- Google Analytics data from offline page views are stored in IndexedDB and pinged to Google Analytics when back online.
The PWA also deals with updates. When loading the app, it checks whether updates are available from the CMS via the Drupal API. It then either prompts the user to go through an update process if data was downloaded into IndexedDB, or performs a simple background cache clean up if data was not downloaded.
More details on what the serviceWorker does
The serviceWorker catches request before they hit the network. What the serviceWorker does depends on the URL that is requested but for most pages of these apps, this is what it does:
- Looks for the resource in the cacheStorage.
- If it is there, serves that resource fetched from the cacheStorage.
- If not, tries to see if the data is in IndexedDB and if the template/view for that page is in the cacheStorage.
- If they are, builds the page from the data and the template and serves that and adds it to cacheStorage.
- If the data and template were not found, passes on the request to the network.
As mentioned above, this behaviour will be adapted to the type of resource requested. For example, search results pages will not be cached. This is to avoid clogging the cacheStorage. API requests to the CMS when checking whether there are updates will go straight to the network.
Our design principles (and a few pre-conceptions to get rid of)
The front-end application of a headless CMS architecture doesn’t have to be a JS app, let alone a single-page app. Ours is in PHP and keeps all the good old features of the web such as URL-based navigation. Content is accessible even if JS is disabled in the browser or couldn’t load.
A PWA definitely doesn’t have to be a single page app. On the contrary, a JS single page app would not work at all if JS were disabled or could not load in the browser. That would go against the principle of progressive enhancement, which partly defines what a PWA is.
A JavaScript app can be written in Vanilla JS, as opposed to using a framework such as React or Vue. When I mentioned to other developers I had been tasked with building a progressive web app and described the project a little bit, I was asked which JS framework/library/packages I was going to use. That question took me aback more than anything. I will happily use React or any other framework when the need arises. I actually spent some time learning React. I’m yet to come across a situation where it will serve the project purpose. This PWA project is almost entirely custom Vanilla JS. The only library we used is lunrjs to perform the search.
Thorough planning goes further than build tools. As you can see from the ’technical’ description above, some functionalities are available to a wide range of browsers, others only to the latest ones. We designed this application with browser-compatibility in mind. Instead of relying heavily on a transpiler (hello Babel!) we segmented our code according to browser support. For example, none of the IndexedDB-related code uses Promises, as IndexedDB and Promises browser support do not match. On the other hand, code within the serviceWorker makes full use of Promises, because where serviceWorkers are supported, Promises will as well. We did have to use Babel in the end as we wanted to be able to write classes to organise our code (which, in hindsight, we could have done without). So in the end, our build tools are limited to:
- a simple NPM script for building theme-related JS (’surface’ DOM manipulation tasks)
- a simple Webpack/Babel combination for the JS application.
Nothing too complicated or fancy, so it is more likely to work on a wide range of browsers, less likely the break down when updating build tools, and more likely to build without any glitches on the machines of other devs. Can I use is your best friend.
No need to work on performance itself if you don’t mess it up in the first place. The best way to ensure performance is to follow a few principles during development as opposed to try and improve performance when you are done. Only load JS files on the page where they are needed. Don’t load whole frameworks if you are only going to use a fraction of them or they do not exactly serve your purpose. Don’t rely too much on build tools that are going to have to expand your code – even if they also minify your JS in the end.
Don’t forget about cache management. It’s important to manage the content of the cacheStorage. This data will accumulate in the users’ browsers and devices. And browsers can be unforgiving when the cache gets saturated. They will remove whole apps in one go according to criteria that are not fully transparent. Make sure you implement cache clean ups when data or your serviceWorker are being updated. Don’t cache anything that doesn’t bring much benefit.
The results?
- A web application with the native-like features described above.
- A JS application that weights 61.3KB (JS files that make up the application only).
- On first request, the sites loads 1.4MB in 2.36s. The second request takes 1.02s and loads 97KB.
- After downloading the site data, page loads take about 805ms on average.
- Data stored in the browsers amount to under 20MB (18MB with all data and images in but without having visited pages yet).
- A project using familiar technologies, based on fairly well-spread knowledge. The only new technologies used are serviceWorkers and cacheStorage, which are native to browsers so we can hope these will be well documented over time and won’t go out of fashion anytime soon.