Are PWAs still relevant?

What is a PWA?

This is why Progressive Web Apps are so appealing to many of us. With them, you can build your web app once, and then use the same code base to offer a native experience on mobile devices.

So, in theory, you can have your cake, and eat it too. You can build apps fast, without wasting too much money on development, and with good end results.

The number of PWA features has increased quite a lot in recent years, so let’s spend the next few minutes building a progressive web app from scratch, and see if this is actually a viable solution these days.

Steps to convert a Web App into a PWA

We’ll start by initializing and running a really basic Solid JS project.

$ npm create solid
$ npm run dev

This results in an index.html page, where a Single Page App is loaded into an app shell.

<html lang="en">
  <head>
    <title>Awesome</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/src/index.tsx" type="module"></script>
  </body>
</html>

The app itself is just a basic counter.

function App() {
  const [count, setCount] = createSignal(0);

  function increase() {
    setCount(count() + 1);
  }

  return (
    <div>
      <h1>{count()}</h1>
      <button onClick={increase}>+1</button>
    </div>
  );
}

Based on this script, the DOM is rendered, and event listeners are attached to the interactive elements. Easy stuff!

To convert this web app into a progressive one we need to follow three easy steps.

Step 1

First, let’s create a manifest file which is essential for providing a native app-like experience on the web.

{
  "name": "The Awesome App",
  "short_name": "Awesome",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ]
}

It allows users to install your app on their devices’ home screens, ensures a consistent look and feel, and provides a full-screen, immersive experience. We’ll have to define a name, the start URL, the preferred display mode, and at least one icon for the app to be installable.

Additionally, we can optionally add a short name used when there’s not enough space to display the full name, and some theme options. Quick side note, the manifest file can contain a variety of other fields to fine-tune your app. For the purpose of this example, we’ll stick with the bare minimum.

Step 2

The second step in this process is to register the manifest file in our app header.

<html lang="en">
  <head>
    <title>Awesome</title>
    <link rel="manifest" href="/manifest.json" />
  </head>
  <body>
    <div id="root"></div>
    <script src="/src/index.tsx" type="module"></script>
  </body>
</html>

Now we can jump back into the browser, and you’ll see the Install app option present in the navigation bar. Once you go through the installation process you should see your web app behaving just like a native app on your device.

Step 3 - Service Workers

Now, for the fun part, let’s define and register a service worker. This is what is going to allow us to add progressive functionalities such as offline support, background sync, or push notifications.

Back in the index.html file, we’ll add a small script in the header, where the worker is loaded if the browser supports this feature.

<html lang="en">
  <head>
    <title>Awesome</title>
    <link rel="manifest" href="/manifest.json" />
    <script>
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker
          .register("/service-worker.js")
          .then((registration) => {
            console.log("Registered!");
          })
          .catch(console.error);
      }
    </script>
  </head>
  <body>
    <div id="root"></div>
    <script src="/src/index.tsx" type="module"></script>
  </body>
</html>

Service worker code runs in the background, separate from the web page, enabling features that don’t need a web page or user interaction.

We’ll take the easy route and use Google’s Workbox to address resource caching. This is mandatory when offering offline support.

I’m downloading the library from the CDN, and this will give us access to a workbox routing object in our scope. Then, we can register routes, and identify what type of resource our app is fetching.

importScripts(
  "https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
);

workbox.routing.registerRoute(
  ({ request }) => request.destination === "image",
  new workbox.strategies.CacheFirst(),
);

Since images are not likely to change very often, we could use a Cache First strategy. In this case, if there is a response in the cache, we’ll use it directly without making a network call. Otherwise, we’ll fall back to the network, and then cache the response for any subsequent request.

importScripts(
  "https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
);

workbox.routing.registerRoute(
  ({ request }) => request.destination === "image",
  new workbox.strategies.CacheFirst(),
);

workbox.routing.registerRoute(
  ({ request }) => request.destination === "script",
  new workbox.strategies.NetworkFirst(),
);

Scripts might be updated more often, so we’ll use a Network First strategy, where we’ll try to fetch from the Network first, and only fall back to the cache if the network fails.

List of PWA features

Ok, but the real question remains - what exactly can you do with PWAs. What you might not realize is that you can achieve a lot of interesting things thanks to the various Web APIs these days, and this website showcases all these capabilities pretty well.

While features like Media Capture

<input type="file" name="selfie" accept="image/*" capture="user" />
<input type="file" name="picture" accept="image/*" capture="environment" />

or Geolocation

navigator.geolocation.getCurrentPosition((position) => {
  const { latitude, longitude } = position.coords;
});

are fairly common on the web these days, you’ll be surprised to find out that your plain old web browser has access to the device’s native position and orientation

window.addEventListener("deviceorientation", (event) => {
  console.log(`${event.alpha} : ${event.beta} : ${event.gamma}`);
});
window.addEventListener("devicemotion", (event) => {
  console.log(`${event.acceleration.x} m/s2`);
});

and can also handle quite complex tasks such as Passwordless Authentication

let credential = await navigator.credentials.create({});

or Speech Recognition.

let recognition = new SpeechRecognition();

If you are still not convinced that PWAs are worth it, let me show you two really interesting success stories.

Twitter’s PWA led to a 65% increase in pages per session, 75% more Tweets, and a 20% decrease in bounce rate, all while reducing the size of their app by over 97%.

Hulu also replaced its platform-specific desktop experience with a PWA and saw a 27% increase in return visits.

If you feel like you learned something, you should watch some of my youtube videos or subscribe to the newsletter.

Until next time, thank you for reading!