This post is the first post of a series of three posts about how I created isitblackfridayyet.app.
Full story here

Create a Stencil app

Creating a app with StencilJS is really easy. All you need is a recent Node and NPM version.

It all starts with the following command:

$ npm init stencil

short after you will be prompt by a choice: Create an Ionic PWA, a StencilJS app or a collection or component.

As I wanted the most lightweight app possible, I picked the second choice, but pick the right choice for your project:

$ Pick a starter › - Use arrow-keys. Return to submit.
   ionic-pwa     Everything you need to build fast, production ready PWAs
❯  app           Minimal starter for building a Stencil app or website
   component     Collection of web components that can be used anywhere

Pick a project name and confirm the creation:

✔ Pick a starter › app
✔ Project name › myprojectname
? Confirm? › (Y/n)

At this point a myprojectname folder is created. Enter the folder, install the dependencies and start the app:

$ cd myprojectname/
$ npm install
$ npm start

A new browser tab should have now opened at http://localhost:3333/ with the following default app. This app has two pages and 3 Web Components, enough to get you started

redirect www
redirect www

Docs

Lighthouse Progressive Web App (PWA) result

Open Chrome devtools at the Audit tab and run a lighthouse report on your app.

lighthouse result after

You should get the above report. Basically, by default a StencilJS app does not qualify to be a Progressive Web App (PWA)… because of the lack of service worker.

Register a new Service Worker into your StencilJS app

In your web page src/index.html, register your new service worker file like so:

<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
  // Use the window load event to keep the page load performant
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js');
  });
}
</script>

Run npm start and open http://localhost:3333. You will get this error:

service worker not found

This error means that service-worker.js file is not present. To add it to the distribution folder (www/), add the following copy property into stencil.config.ts file.

export const config: Config = {
  copy: [{ src: 'service-worker.js' }]
};

now reload the page, the error should be gone.

Import Workbox

Workbox is a set of libraries and Node modules that make it easy to cache assets and take full advantage of features used to build PWAs.

Edit src/service-worker.js and add the following code:

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

if (workbox) {
  console.log(`Yay! Workbox is loaded 🎉`);
} else {
  console.log(`Boo! Workbox didn't load 😬`);
}

It basically import workbox from a CDN and test if it exists or not.

Stop the webserver and reload npm start. Open the console, Yay! Workbox is loaded 🎉 should be displayed.

Debug

The “update on reload” toggle will force Chrome to check for a new service worker every time you refresh the page. This means that any new changes will be found on each page load.

use chrome devtools to debug service workers

If you want to see the logs even on prod, you can use workbox.setConfig({ debug: true });. See the result on isitblackfridayyet.app

Assets caching strategies

Workbox has 5 different assets caching strategies:

  • StaleWhileRevalidate: Get from the cache if available, fallback to the network, then populate the cache in the background.
  • CacheFirst: Get from the cache if available and fallback to the network is empty
  • NetworkFirst Get from the Network first and fallback to the cache if offline
  • NetworkOnly Bypass the cache
  • CacheOnly Bypass the Network

JS files

If we want our JavaScript files to come from the network whenever possible, but fallback to the cached version if the network fails, we can use the NetworkFirst strategy to achieve this.

network first strategie explained
workbox.routing.registerRoute(
  /\.js$/,
  new workbox.strategies.NetworkFirst()
);

CSS files

To serve CSS from the cache and updated in the background use StaleWhileRevalidate

StaleWhileRevalidate strategie explained
workbox.routing.registerRoute(
  // Cache CSS files.
  /\.css$/,
  // Use cache but update in the background.
  new workbox.strategies.StaleWhileRevalidate({
    // Use a custom cache name.
    cacheName: 'css-cache',
  }),
);

Google Fonts

isitblackfridayyet.app uses Alata font. Fonts are sometimes slow to load, so it is a good pratice to cache the CSS and the Font file.

Caching CSS

// Cache the Google Fonts stylesheets with a stale-while-revalidate strategy.
workbox.routing.registerRoute(
  /^https:\/\/fonts\.googleapis\.com/,
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'google-fonts-stylesheets',
  })
);

Caching the font file with CacheFirst strategie

cache first strategie explained
// Cache the underlying font files with a cache-first strategy for 1 year.
workbox.routing.registerRoute(
  /^https:\/\/fonts\.gstatic\.com/,
  new workbox.strategies.CacheFirst({
    cacheName: 'google-fonts-webfonts',
    plugins: [
      new workbox.cacheableResponse.Plugin({
        statuses: [0, 200],
      }),
      new workbox.expiration.Plugin({
        // a year in seconds
        maxAgeSeconds: 60 * 60 * 24 * 365,
        maxEntries: 30,
      }),
    ],
  })
);

Assets precaching

To be even more performant we can cache assets ahead of time. Precaching a file will ensure that a file is downloaded and cached before a service worker is installed.

Add the following line to src/service-worker.js file.

workbox.precaching.precacheAndRoute([]);

the precacheAndRoute function will need to be populated, post build with the list of static assets generated and their versions as seen below.

[
  {
    "url": "favicons/android-chrome-192x192.png",
    "revision": "c51604a2fd213e799bdd79ba14087001"
  }
]

To get this list easily, we need to install Workbox CLI

Install Workbox CLI

npm install workbox-cli --global

Now build you Stencil app npm run build and run workbox wizard --injectManifest

? What is the root of your web app (i.e. which directory do you deploy)? www/

#1. Choose www/

? Which file types would you like to precache?
❯◉ png
 ◉ xml
 ◉ ico
 ◉ svg

#2. Select the assets you want to cache

? Where's your existing service worker file? To be used with injectManifest, 
it should include a call to 'workbox.precaching.precacheAndRoute([])' www/service-worker.js

#3. Point to www/service-worker.js file that should be there if you followed from the beginning

? Where would you like your service worker file to be saved? www/sw.js

#4. Select a new name for the generated service worker (can’t be www/service-worker.js). Here www/sw.js is fine.

Don’t forget to change the name of the service worker you want to use on prod with the new one:

window.addEventListener('load', () => {
  navigator.serviceWorker.register('/sw.js');
});
? Where would you like to save these configuration options? (workbox-config.js)

#5. Save the config at the root of your project for future usage.

To finish run workbox injectManifest to populate workbox.precaching.precacheAndRoute([]) line

$ workbox injectManifest

At this point you app should be able to run offline. Try it out on isitblackfridayyet.app.

Everything together

importScripts(
  'https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js',
);
workbox.setConfig({
  debug: true,
});

workbox.routing.registerRoute(/\.js$/, new workbox.strategies.NetworkFirst());

workbox.routing.registerRoute(
  // Cache CSS files.
  /\.css$/,
  // Use cache but update in the background.
  new workbox.strategies.StaleWhileRevalidate({
    // Use a custom cache name.
    cacheName: 'css-cache',
  }),
);

// Cache the Google Fonts stylesheets with a stale-while-revalidate strategy.
workbox.routing.registerRoute(
  /^https:\/\/fonts\.googleapis\.com/,
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'google-fonts-stylesheets',
  }),
);

// Cache the underlying font files with a cache-first strategy for 1 year.
workbox.routing.registerRoute(
  /^https:\/\/fonts\.gstatic\.com/,
  new workbox.strategies.CacheFirst({
    cacheName: 'google-fonts-webfonts',
    plugins: [
      new workbox.cacheableResponse.Plugin({
        statuses: [0, 200],
      }),
      new workbox.expiration.Plugin({
        maxAgeSeconds: 60 * 60 * 24 * 365,
        maxEntries: 30,
      }),
    ],
  }),
);

workbox.precaching.precacheAndRoute([]);

Result

Open the devtools, under Application > Cache Storage you should see a bucket for precached files and some more for CSS, JS etc.

service worker's cache buckets

If you open the Network tab and reload your app you should see the assets served by the service’s worker cache

service worker not found

And to finish re-run the lighthouse audit:

lighthouse result after

You are now the proud owner of a great PWA!