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
Lighthouse Progressive Web App (PWA) result
Open Chrome devtools at the Audit
tab and run a lighthouse
report on your app.
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:
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.
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 emptyNetworkFirst
Get from the Network first and fallback to the cache if offlineNetworkOnly
Bypass the cacheCacheOnly
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.
workbox.routing.registerRoute(
/\.js$/,
new workbox.strategies.NetworkFirst()
);
CSS files
To serve CSS from the cache and updated in the background use StaleWhileRevalidate
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 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.
If you open the Network
tab and reload your app you should see the assets served by the service’s worker cache
And to finish re-run the lighthouse audit:
You are now the proud owner of a great PWA!