Over the past while, I’ve been working on a sample Progressive Web App with @Mustafa_x, designer extraordinaire and overall incredibly patient man (judging by how long he had to wait for me to finish coding it). Our goal was to make something from scratch, with a strong focus on UX. I took it as a chance to dive deep into a number of web development aspects and rewrite my code a number of times while doing so. Hence the wait. Sorry, Mustafa.
So what went into making this PWA? Here are a few highlights:
- Vanilla (no frameworks). I wanted to see what it took to develop a nice-feeling application without using any frameworks or complex runtime libraries. As a single-page application, this was quite doable; it may be harder for larger applications, but I hope to explore that at some point.
- Smooth. The goal is for the app to always hit 60fps. Transitions are CSS-driven with JS orchestration, which works quite well, given that the app is not scripting-heavy, and the main thread is usually free. The app also uses Material Components for the web for the UI elements, that have themselves been built with performance in mind. Beyond that, there are a number of interesting tricks in there to speed things up, such as using proxy elements in animations.
- Custom build pipeline, using gulp and a number of great plugins. This allowed me to run Babel for transpilation (and minification, with babili), rollup for module bundling, and postcss for CSS custom property replacement and minification. More importantly, it allowed me to use all of these tools at exactly the right point in the pipeline, so that I could generate incremental targets for lazy-loading that are still fully processed, transpiled and minified.
Material Money should work well in any modern browser, as long as either IndexedDB or local storage are supported, and modern CSS features like flexbox and
calc() are available and unprefixed. Edge, Firefox, Chrome and Safari should all work well.
I won’t lie: it was challenging making this app and keeping the focus on polish. Very often, the easy path on the web is not the best one, and there’s a significant amount more work involved in doing the right thing.
Here are a few examples, some of which I hope to expand into full blog posts:
Animation on the web is not straightforward, even if all you’re trying to do is a simple CSS transition; many properties cause repaints on every frame of an animation, or even worse, layout passes, and depending on your DOM and how many pixels are changing, this may cause janking. In fact, pretty much only
transformare guaranteed to produce performant animations across all browsers, without triggering repaints or causing layout recalculations, and even that isn’t the full story (check CSS Triggers for details). Material Money achieves all of its animation effects with just
transform, often with a healthy dose of sleight-of-hand.
Service Workers are hard. If you’re trying to code your own without using a library like sw-toolbox, you’re going to run into pain points trying to debug all the possible code paths. Promises definitely make things easier, but asynchronous code is never straightforward, especially when triggering a particular code path potentially requires a lot of setup. Even then, if you’re not careful, it’s easy to fall into issues with breakage due to different resources being on different versions, loading things from browser cache when you expect a fresh copy from the network, or accidentally stopping users from getting updates due to a bad choice of caching headers. Do your homework, and be prepared for a steep learning curve.
Hand-crafting your critical path CSS involves a far bit of work, but can be a really powerful way to split up an application and reduce how long it takes to get to that first render. If you’re using external CSS, such as a UI library, you might be able to remove a lot of code from your initial payload and in doing so get something on the user’s screen as quickly as possible. The rest you can just lazy-load as needed or when idle. Split your CSS into critical and non-critical, use
@importto pull in the external code, and set up your build process to concatenate and minify both separately, later inlining the critical into your HTML.
I learned a lot with Material Money, and I’ll definitely be putting out some posts and articles going into the details of the problems I faced and how I chose to solve them. My choices aren’t necessarily the right ones for everyone, but having something that works as a reference may help you find your own path.
Other than that, I’d like to develop the model implementation a bit further while keeping it simple, as well as explore the intricacies of developing a larger-scale application, while keeping things vanilla, fast, and as polished as possible.
I’m sure there are bugs in there, and I’d love your help in finding and squashing them. Even so, I hope that it’s already enough to give you some ideas for your next app! Happy source reading, and get in touch if you have any questions!