January 11, 2021Justin Willis
There are MANY ways to build great Progressive Web Apps, but today I wanted to share an approach that we have been exploring, building PWAs with Web Components based solutions! Specifically, we will be discussing the following technology stack:
- lit-element: Our framework. Lit-element gives us a way to write code that feels remarkably familiar to popular frameworks like React but that compiles down to browser native Web Components with a tiny runtime that provides things such as performant asynchronous rendering.
- Shadow DOM, CSS Variables, Shadow Parts: Modern CSS is incredibly powerful, especially when combining the Shadow DOM, CSS variables and the Shadow Parts APIs. We will discuss how this provides everything we normally were using a CSS pre-processor, but without the extra complication that a CSS pre-processor adds to your build steps!
- Rollup: Rollup is a ΓÇ£bundlerΓÇ¥ or build tool that will make working with NPM modules easy while also helping ensure our code is ready for production. It allows us to do things such as minify our code, run Workbox (another tool I will introduce next) and other build steps.
- Workbox: Workbox is a tool that makes working with Service Workers easy!
- TypeScript: TypeScript gives us features such as auto complete in our editors that helps make the development process easier, along with being perfect for working in a team because you can provide types for your APIs, making your code almost self-documenting.
- PWABuilder: PWABuilder is there to help you get your PWA store ready and then easily ship your PWA to those stores! We provide easy ways to evaluate the quality (performance, security, Web Manifest content etc) of your PWA, improve if necessary (such as adding screenshots and other necessary content to your Web Manifest) and then easily package your PWA for app stores. For example, with just a few button clicks you can have a package that is ready to ship to both the Google Play Store and the Microsoft Store!
A quick note before we dive into the content: Our goal with sharing this with the community is not to say this is the only way to build web apps, or that it is even the absolute best way to build web apps, instead we wanted to share what works well for us in the hopes that it can help drive your teamΓÇÖs decisions around what works well for you and your application.
Setting our Goals
When you are going to build a new app (of any kind) there are a series of decisions that must be made up front. These decisions normally involve picking a technology stack based on some set of goals for the project. With PWAs this becomes even more important as there are many frameworks, libraries, build tools and more to choose from and these choices can have large impacts on your PWA as you develop it. For example, if loading performance is a key metric for your application then the framework that you choose to build the application in at the beginning can have a significant impact on loading performance farther down the road. For the PWABuilder team our key goals look something like the below:
- Performance: Our current tech stack and codebase produces large bundles which can take seconds to not only load over the network (before our service worker has cached anything) but also to be parsed by the browser, which must happen before code can start executing. This is not only not great for our users, but also is not a shining example of best practices on the web.
- Maintainability: Our current build is made up of multiple tools, a large WebPack config with many plugins, a CSS pre-processor, multiple CSS libraries and utilities, Polyfills, Redux for state management etc. This not only is a large reason of why we have the bundle size issues discussed above but also has had a substantial impact on development speed for the front end as we end up debugging our tooling or codebase more than building the quality features we know developers want. As we have rebuilt our backend over the last year, along with other PWAs we have built outside of pwabuilder.com we have seen that simplicity is key to maintainability.
- Quality: As with any software project, the harder the codebase is to work on, the buggier it tends to be. Because of the above two challenges, we have also had to deal with a fair bit of tech debt. We think with our new focus on simplicity we can move much faster while still maintaining a high level of quality.
- Developer experience: Maintaining a familiar developer experience is also incredibly important to us as it allows both community members and new members on the PWABuilder team to easily jump into the project and understand what is going on.
Based on the above we have found that browser native, Web Components based solutions combined with tooling that is focused on simplicity and sticking to web standards gives us the easiest path to meeting these goals. Let’s dive into the details of this approach!
Web Components? lit-element?
What are Web Components?
First, let’s touch on what Web Components are. Web Components are a collection of Web APIs that allow you to build components. You can think of this as the same as components you build with something like React or Angular, but there are some key differences with Web Components. They are currently supported in all browsers besides Internet Explorer.
What is lit-element?
Lit-element is a library / framework that makes it easy to build Web Components. Lit-element gives us a way to write code that feels very familiar to popular frameworks like React but with Web Components. It also includes a tiny runtime that provides things such as performant asynchronous rendering. Also, lit-element is now only one of many Web Components based frameworks / libraries that are available. We also recommend checking out fast-element and Stencil!
Why Web Components?
First, components built with either React or Angular are not native components understood by browsers. Because of this, you need to ship a large runtime (the core of both React and Angular) of some sort that can run these components in the browser. Note that this is a basic explanation of how these frameworks work, there are of course many details in the actual implementations, but these details are not truly relevant to our discussion today. This runtime cost is a huge part of the loading performance issues that apps built with these frameworks commonly have.
With Web Components based frameworks you avoid this large runtime cost as the browser natively understands Web Components, but without sacrificing developer experience. This enables frameworks to still ship features such as performant async rendering but without the code associated with just running the components. For example, let’s use BundlePhobia Γ¥ÿ cost of adding a npm package to evaluate the cost of React, Angular and lit-element:
- minified: 121.1 kB
- minified + GZip: 39.4 kB
- Load time on a 3G connection (one of the most common network connections): 0.79s
- Minified: 289.9 kB
- Minified + GZip: 88.7 kB
- Load time on a 3G connection (one of the most common network connections): 1.77s
and then lit-element, a Web Components based solution:
- Minified: 23.2 kB
- Minified + GZip: 7.4 kB
- Load time on a 3G connection (one of the most common network connections): 148ms
Lets now compare a Web Component built with lit-element to the same component built with React:
Based on this, you can see that the developer experience is extremely similar but with a much smaller bundle for the Web Component!
- Maintainability / Stability
Web Components, by their nature of being Web Standards, are inherently going to be more stable than any custom code. For example, the Geolocation API is a web standard and because of this, once it was implemented in a browser it has consistently worked since and we can count on it continuing to work far into the future because of the webs guarantee on not breaking existing websites. There are of course drawbacks with this approach, such as it taking longer for Web Standards to be implemented compared to the React team implementing a new feature in React. However, this is a tradeoff that we feel is fair in return for a good guarantee on long term stability.
Styling with Web Components
Continuing with our Web Standards first approach, modern CSS now has built in APIs / features in browsers that give us all the features we normally use a CSS pre-processor such as SASS for:
- Variables: CSS now has CSS variables! CSS variables work great with Web Components and work extremely similar to variables in SASS.
- Style Encapsulation: Shadow DOM is part of the Web Components spec and helps fix the classic CSS cascade issue. Shadow DOM enables you to encapsulate your styles “inside” of your component, ensuring that CSS elsewhere in your app does not accidentally override CSS in your component. However, using both CSS Variables and the Shadow Parts API we can enable specific pieces of our components to be style-able from outside of the component. This is helpful when you want a component to have certain styles customizable but still have default styles too.
Luckily, by basing the rest of our tech stack on Web Standards it also makes setting up our build tools easy!
For our bundling we use Rollup which is described on the Rollup Website as:
As you can guess, lit-element also uses standard built-in ES modules and because of this it works seamlessly with Rollup! Rollup also runs other tasks that are important for our builds:
- Minification of our code
- Removing console logs
- We use the node-resolve plugin to ensure we can easily use modules from NPM that may not be distributed as an ES module.
- Workbox (covered below)
Workbox (Service Worker)
We use Workbox, which is a tool that makes working with Service Workers easy! Workbox enables you to build fully customized offline experiences for your app while also simplifying many of the complicated parts of Service Workers, such as pre-caching. We also use the Workbox Rollup plugin which makes it super easy to do pre-caching of our assets which not only is a huge part of the offline experience, but also ensures that our PWAs load even faster after the user has loaded the app at least once! Better performance AND an offline experience with only a few lines of code is always awesome!
- Share and receiving shared content: Using the Web Share API your PWA can tie into the native share UI of the users Operating System. Using the Share Target API your PWA can receive content shared from another app. For example, I can share an image straight from the native Photos app on Windows to a PWA.
- File System Access API: If you have built a hybrid Web App you are familiar with how hard it is to integrate with the file system, but no more! The File System Access API gives you all the functionality native apps have! With a permission grant from the user, you can then read or save changes directly to files and folders on the userΓÇÖs device!
- Periodic Sync API: This allows PWAs to sync data in the background, just like a native app. This ensures that your users always have the latest data on their device.
And this is just a tiny bit of what’s possible! For the full list check the Project Fugu status page! New capabilities status (web.dev)
Get started building a PWA with this tech stack!
If your goals are similar to ours and you want to give this tech stack a try you should check out the PWABuilder pwa-starter . This starter includes everything we discussed above and has everything set to what we think are good defaults. Also, if you are looking for a tutorial feel free to check out this video for a walk through on using the starter.
Alright, that is how we are building PWAs fully with web components! Remember to check out the starter above if this tech stack interests you and please feel free to add any feedback as an issue on the repo. And, once your PWA is ready to go donΓÇÖt forget to come back to PWABuilder to get your PWA in the app stores! The PWABuilder team is going to be rebuilding PWABuilder with this tech stack for 2021, and we are very excited to show you the results when we are done! The web and the amazing community around it have us excited to see where PWAs go this year!