To PWA or not To PWA? Our latest field experience
A few months ago we had the chance to build a mobile application for a major energy distribution company involved in e-mobility.
The company builds and distributes an e-mobility management dashboard integrated with all the major e-charging service vendors, allowing them to control the charging units distributed across the territory, monitor their parameters, manage contracts and provide complex data visualisations.
The goal was to provide quick access to a restricted set of functionalities of the portal. In particular, we had to provide a way to quickly research the charging units registry of the system for a particular Charging Unit, configure new Charging Units installed on the territory and interact with them via remote commands to test the successful start/stop of a recharge.
The challenge was to help operators complete the intended tasks on a smartphone screen, which is quite different from the information-heavy dashboard they were used to.
We achieved the goal by designing and implementing a Progressive Web App that enables operators to complete configuration tasks with the guidance of a wizard. The app relies on HTML5 features to reduce the manual work required as much as possible.
Working on this project has been a particularly rewarding experience for us: we had the possibility to design the UX from scratch and also face many interesting technical aspects of implementing a mobile web app.
With this article, we want to dive into the tech stack we adopted, how it empowered us to achieve the desired user experience and ultimately share the lessons we learned along the way.
Why building a Progressive Web App?
- Thanks to the power of HTML5, PWAs have access to a lot of device hardware and features: geolocation, camera and more can be accessed through standard web APIs. PWAs integrate with the mobile OS to provide a UX similar to that of a native app, enabling push notifications and the possibility to add the app icon to the device home screen;
- A PWA has a lower friction of distribution than a native app. We were able to setup as many environments (testing, QA…) as we needed, without having to subscribe to any native app beta distribution platform or program;
- Publishing a new version is as simple as deploying a bunch of static assets, without any approval process; since we leverage our Kubernetes cluster for deployments we are able to automate the publishing phase in a CI pipeline, and get rolling updates, blue/green deployments and rollbacks for free;
- As a consequence, it is also easier to link a PWA (it is just a simple URL), bookmark and access it from multiple sources.
This said, PWAs have some limitations (e.g., as of today there is no mechanism to make a PWA the target of an Intent for inter-app communication) which is important to be aware of. Luckily, modern browsers support all the features we needed to build a successful mobile Charging Unit configurator!
Building a mobile Charging Unit configurator
From the very beginning, one of the noteworthy challenges of building this application was to adapt in an effective way the existing feature-rich desktop dashboard to a more constrained mobile environment. In particular, we faced three challenges related to both UX and technical aspects:
- Operators had to be able to search for a particular Charging Unit by using its serial number, which is a long alphanumeric identifier; this would have been pretty cumbersome on mobile;
- Operators had to be able to configure a new Charging Unit by filling a lot of information. We needed to make this process as lean as possible, presenting the information in a compact way;
- Our implementation would have to take into account volatile mobile devices network, which are generally slower than desktop devices; we needed to optimise network requests without changing the existing APIs, and be resilient for the cases in which there would be no connectivity.
Leveraging mobile capabilities
We overcome these issues by leveraging the device capabilities as much as possible.
We wanted a better way to search for an existing Charging Unit than having to input a long serial number. Luckily, each Charging Unit comes with a QR Code which encodes exactly that information! We used the device camera and implemented a QR Code scanner that would automatically recognise the Charging Unit.
We organised the data to input for the configuration in three steps, which were implemented as a guided wizard; we used device features such as GPS and integrated the Google Maps API to automatically fill in all location-related information.
Finally, we wanted our app to be offline-first: resilient to flaky networks, able to display at least some feedback when the device went offline, with a caching mechanism to avoid repeating API calls for data that rarely change (such as the list of asset providers, the images of the Charging Unit models and similar resources).
The tech to make it happen
The PWA is built on top of create-react-app v1, which provides a ReactJS + Webpack scaffolding with sensible defaults. For the styling part, we built a micro design system by composing TailwindCSS classes; we wrote almost 0 lines of CSS and still have a nice and consistent look across the whole application.
By using zxing-js for the QR Code recognition part we were able to easily integrate serial number decoding with the camera video stream, having only to be careful about excluding unnecessary transitive dependencies from the final build with some Webpack magic (since zxing-js is an isomorphic library it contains some server-side only dependencies). We encapsulated the QR recognition logic in a dedicated React component, which is lazy-loaded through a dynamic import, to keep the initial bundle size as small as possible.
Similarly, we leveraged the navigator.geolocation API to retrieve the GPS coordinates, use them in a request to Google Maps API to display a human readable address and a map of the area. As for QR code detection, this code is lazy loaded on demand only when the geolocation feature is activated.
For offline-first support, in the past we implemented data caching and rehydration with redux-offline in our previous applications. Unfortunately, its usage clutters the application code with its dedicated redux actions format. Furthermore, it caches only data in the redux store, while we wanted control over potentially every response coming from the network (especially images): we needed a solution that would be transparent to the application business logic. This is where service workers come into play.
There are only two hard problems in CS: service workers and cache invalidation
Citing the Google Developers website,
a Service Worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don’t need a web page or user interaction.
While they enable many features (like push notifications), their core capability is acting as network request interceptor and handler (think of a in-browser proxy). By combining the Cache API, a service worker can be programmed to cache incoming network responses; by intercepting network requests, they can avoid going over the wire by returning immediately a previously cached response.
This means that a PWA with a service worker can cache all its static assets to make the app usable even in lack of network connectivity, and optimise the number of outgoing requests.
Even though this opens a whole lot of new scenarios, service workers should be activated with care. A web app with a service worker is not simply a set of documents with remote URIs, but behaves more like a native app: the browser will take extra measures to ensure data consistency and code consistency. This basically means that the service worker changes the usual behaviour of the browser to ensure that the web app does not break unexpectedly across updates (this article contains a detailed explanation).
Among the most unexpected and immediate consequences of this change is the fact that service workers break the refresh button! There are many ways to work around this issue; their implementation has varying degrees of difficulty, and depending on how the web app is used one will be probably more suitable than the others. Unfortunately, the choice requires an understanding of all of them, and coding the solution by combining several different browser APIs.
There is only one hard problem in CS: easy caching strategies
After some dig into these issues, we concluded that rolling out a custom service worker implementation would not be sustainable in the long run. So we relied on Workbox, a set of service worker libraries provided by Google in their effort to promote PWAs.
In particular, we leveraged the Workbox Webpack Plugin, which can generate a service worker that caches all static files produced by the Webpack build. In addition, the plugin allows to declaratively configure the service worker behaviour for additional resources; combined with the Workbox Caching Strategies module, it made possible for us to easily configure the caching behaviour of all API responses. This is how an example configuration looks like:
Here, the Stale While Revalidate strategy means that the service worker will immediately return the cached response as long as it is still considered fresh (it has been less than one week since it has been cached). Additionally, it will still issue the network request in the background to update the cache.
There are several other strategies available in Workbox to suit different use cases, such as Network Only, Cache Only, Network First and Cache First.
The Platform is the limit
Building a full-fledged app with web APIs and have it always available in the browser feels great. By applying advanced patterns and the right tooling, it is possible to achieve a result which comes pretty close to the experience of a native app.
Yet, we had some aches while building our PWA.
For example, caching and the refresh strategy are not the only problems to take into account when enabling service workers; there are many additional aspects to take into account, such as the service worker relationship with code-split bundles. Even with the help of libraries and frameworks, developers should be fully aware of how their application is working and what are the implications of adopting this technology.
Furthermore, so far the experience with PWAs is inconsistent across mobile operating systems; while for Android the support is pretty great, iOS Safari implemented service workers only recently, and debug/develop capabilities in Safari are not as sophisticated as in Chrome. We also experienced difficulties with the OS behaviour itself, since Safari implements a swipe back gesture that cannot be disabled and makes the implementation of some custom gestures impossible, such as reliably dragging a slider from the right edge of the screen.
Today it’s possible to build great experiences for the mobile web, that are well integrated with the device and respect user expectations.
The finished PWA provides an easy to use tool for operators to achieve their goals. Taking only 140 kB on device storage, it is fast and performant even on older devices, thanks to a combination of lazy-loading and service worker caching.
The following paragraph collects some of the link that we found useful in our journey to explore Progressive Web Apps. In addition, we also talked of the struggles of building performant mobile experiences and the state of web APIs for building applications at Frontenders Ticino, slides are available here.
- PWA Stats, a list of stats and news regarding PWAs;
- The offline cookbook, containing code recipes for a number of service worker patterns;
- The Workbox Window proposal, an API to make interaction with service workers simpler;
- Progressive Tooling, a list of tools to improve web performance;
- PWAs as multi page applications;
- Ionic PWA, an Angular PWA Toolkit, and StencilJS, based on web components;
- Twitter PWA Case Study
- Starbucks PWA Case Study
- Tinder PWA Case Study
- Pinterest PWA Case Study