Jeremy Wagner — https://jlwagner.net/ NDC Porto — Porto, Portugal — April 2022

A quick service worker primer.

A tale of two navigation patterns.

Streams!

Where are you going with this?

Streams! Again!

The anatomy of a website.

Header Content Footer

Precache!

npm install \ workbox-navigation-preload \ workbox-strategies \ workbox-routing \ workbox-precaching \ workbox-streams \ —save

// ./src/sw.js import * as navigationPreload from “workbox-navigation-preload”; import { CacheFirst, NetworkFirst, StrategyHandler } from “workbox-strategies”; import { registerRoute, Route } from “workbox-routing”; import { matchPrecache, precacheAndRoute } from “workbox-precaching”; import { strategy as composeStrategies } from “workbox-streams”;

// ./src/sw.js import * as navigationPreload from “workbox-navigation-preload”; import { CacheFirst, NetworkFirst, StrategyHandler } from “workbox-strategies”; import { registerRoute, Route } from “workbox-routing”; import { matchPrecache, precacheAndRoute } from “workbox-precaching”; import { strategy as composeStrategies } from “workbox-streams”; // Enable navigation preload navigationPreload.enable();

https://developer.chrome.com/blog/navigation-preload/

// ./src/sw.js import * as navigationPreload from “workbox-navigation-preload”; import { CacheFirst, NetworkFirst, StrategyHandler } from “workbox-strategies”; import { registerRoute, Route } from “workbox-routing”; import { matchPrecache, precacheAndRoute } from “workbox-precaching”; import { strategy as composeStrategies } from “workbox-streams”; // Enable navigation preload navigationPreload.enable();

// ./src/sw.js import * as navigationPreload from “workbox-navigation-preload”; import { CacheFirst, NetworkFirst, StrategyHandler } from “workbox-strategies”; import { registerRoute, Route } from “workbox-routing”; import { matchPrecache, precacheAndRoute } from “workbox-precaching”; import { strategy as composeStrategies } from “workbox-streams”; // Enable navigation preload navigationPreload.enable(); // Establish cache names const runtimeCacheName = “jlwagnernet-runtime”; const contentCachename = “jlwagnernet-content”;

// Prior code omitted… // … precacheAndRoute([ { url: “/partial-header.php”, revision: PARTIAL_HEADER_HASH }, { url: “/partial-footer.php”, revision: PARTIAL_FOOTER_HASH }, { url: “/offline/index.php”, revision: OFFLINE_FALLBACK_HASH }, …self.__WB_MANIFEST ]);

// Prior code omitted… // … precacheAndRoute([ { url: “/partial-header.php”, revision: PARTIAL_HEADER_HASH }, { url: “/partial-footer.php”, revision: PARTIAL_FOOTER_HASH }, { url: “/offline/index.php”, revision: OFFLINE_FALLBACK_HASH }, …self.__WB_MANIFEST ]);

// Prior code omitted… // … const contentStrategy = new NetworkFirst({ cacheName: contentCachename, plugins: [ { requestWillFetch: ({ request }) => { const headers = new Headers(); headers.append(“X-Content-Mode”, “partial”); return new Request(request.url, { method: “GET”, cache: “default”, headers }); }, // Error handler omitted… } ] });

// Prior code omitted… // … const contentStrategy = new NetworkFirst({ cacheName: contentCachename, plugins: [ { requestWillFetch: ({ request }) => { const headers = new Headers(); headers.append(“X-Content-Mode”, “partial”); return new Request(request.url, { method: “GET”, cache: “default”, headers }); }, // Error handler omitted… } ] });

// Prior code omitted… // … const contentStrategy = new NetworkFirst({ cacheName: contentCachename, plugins: [ { requestWillFetch: ({ request }) => { const headers = new Headers(); headers.append(“X-Content-Mode”, “partial”); return new Request(request.url, { method: “GET”, cache: “default”, headers }); }, // Error handler omitted… } ] });

<?php // Get all includes require_once($_SERVER[“DOCUMENT_ROOT”] . “/includes/utils.php”); require_once($_SERVER[“DOCUMENT_ROOT”] . “/includes/site-header.php”); require_once(“./content.php”); require_once($_SERVER[“DOCUMENT_ROOT”] . “/includes/site-footer.php”); // Check if navigation reload is enabled $isPartial = isPartial(); // Get assets $assets = getAssets(); // Includes if ($isPartial === false) { siteHeader($assets, $isPartial); } content($isPartial); if ($isPartial === false) { siteFooter($assets); } ?>

<?php function isPartial () { return ( isset($_SERVER[“HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD”]) && $_SERVER[“HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD”] === “true” ) || ( isset($_SERVER[“HTTP_X_CONTENT_MODE”]) && $_SERVER[“HTTP_X_CONTENT_MODE”] === “partial” ); } ?>

// Prior code omitted… // … const contentStrategy = new NetworkFirst({ cacheName: contentCachename, plugins: [ { // requestWillFetch handler omitted… handlerDidError: async ({ request }) => { const cacheMatch = await caches.match(request.url, { cacheName: contentCachename }); if (cacheMatch === undefined) { return await matchPrecache(“/offline/index.php”); } return cacheMatch; } } ] });

https://developer.chrome.com/docs/workbox/access-caches-from-the-window/

// Prior code omitted… // … const navigationHandler = composeStrategies([ () => matchPrecache(“/partial-header.php”), ({ event }) => contentStrategy.handle(event), () => matchPrecache(“/partial-footer.php”) ]);

// Prior code omitted… // … registerRoute(({request}) => request.mode === ‘navigate’, navigationHandler);

// Prior code omitted… // … const staticAssets = /.(m?js|css|webp|avif|svg|jpe?g|png|gif|ico)$/i; const staticRoute = new Route(({ url }) => { return staticAssets.test(url); }, new CacheFirst({ cacheName: runtimeCacheName })); registerRoute(staticRoute); // That’s all!

Is this all worth it?

No service worker. With service worker.

fi What’s the eld data story?

https://developer.chrome.com/blog/navigation-preload/

FCP 2,721 ms ❌ 584 ms 90th percentile

LCP 2862 ms ❌ 2059 ms 90th percentile

TTFB 1848 ms ❌ 187 ms 90th percentile

Caveats.

Every page is entitled to a title.

<title> Contact &mdash; jlwagner.net </title>

<title> <?php if ($isPartial === false) { echo $title . “—jlwagner.net”; } ?> </title>

<?php if ($isPartial === true) { ?> <script> document.addEventListener(“DOMContentLoaded”, () => { const { title } = JSON.parse(document.getElementById(“page-data”).textContent); document.title = `${title}—jlwagner.net`; }); </script> <?php } ?>

<script type=”application/json” id=”page-data”> { “title”: “Contact” } </script>

Spinners gonna spin.

/* This rule assumes your header partial ends with an open <main> element. */ main:empty { content: “Loading…”; }

/* This rule assumes your header partial ends with an open <main> element. */ main:empty { content: “Loading…”; }

<script> const effectiveType = navigator?.connection?.effectiveType; if (effectiveType !== “4g”) { document.documentElement.classList.add(“slow”); } </script>

<div id=”loading” aria-label=”Loading page…”> <div class=”dot” aria-hidden></div> <div class=”dot” aria-hidden></div> <div class=”dot” aria-hidden></div> </div>

/* Don’t show the spinner initially. / #loading { display: none; height: 2rem; } / Loading animation styles and other particulars omitted for brevity… / / Show the spinner if stuff’s slow. */ .slow #loading { justify-content: center; display: flex; align-items: center; align-content: center; }

https://philipwalton.com/articles/smaller-html-payloads-with-service-workers/

https://developer.chrome.com/blog/beyond-spa/

https://alistapart.com/article/now-thats-what-i-call-service-worker/

Thank you.