Now THAT’S What I Call Service Worker!

A presentation at NDC Porto 2022 in April 2022 in Porto, Portugal by Jeremy Wagner

Slide 1

Slide 1

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

Slide 2

Slide 2

Slide 3

Slide 3

A quick service worker primer.

Slide 4

Slide 4

Slide 5

Slide 5

Slide 6

Slide 6

A tale of two navigation patterns.

Slide 7

Slide 7

Streams!

Slide 8

Slide 8

Where are you going with this?

Slide 9

Slide 9

Slide 10

Slide 10

Slide 11

Slide 11

Streams! Again!

Slide 12

Slide 12

The anatomy of a website.

Slide 13

Slide 13

Header Content Footer

Slide 14

Slide 14

Precache!

Slide 15

Slide 15

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

Slide 16

Slide 16

// ./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”;

Slide 17

Slide 17

// ./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();

Slide 18

Slide 18

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

Slide 19

Slide 19

Slide 20

Slide 20

Slide 21

Slide 21

// ./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();

Slide 22

Slide 22

// ./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”;

Slide 23

Slide 23

// 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 ]);

Slide 24

Slide 24

// 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 ]);

Slide 25

Slide 25

// 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… } ] });

Slide 26

Slide 26

// 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… } ] });

Slide 27

Slide 27

// 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… } ] });

Slide 28

Slide 28

<?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); } ?>

Slide 29

Slide 29

<?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” ); } ?>

Slide 30

Slide 30

// 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; } } ] });

Slide 31

Slide 31

Slide 32

Slide 32

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

Slide 33

Slide 33

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

Slide 34

Slide 34

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

Slide 35

Slide 35

// 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!

Slide 36

Slide 36

Is this all worth it?

Slide 37

Slide 37

No service worker. With service worker.

Slide 38

Slide 38

fi What’s the eld data story?

Slide 39

Slide 39

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

Slide 40

Slide 40

FCP 2,721 ms ❌ 584 ms 90th percentile

Slide 41

Slide 41

LCP 2862 ms ❌ 2059 ms 90th percentile

Slide 42

Slide 42

TTFB 1848 ms ❌ 187 ms 90th percentile

Slide 43

Slide 43

Caveats.

Slide 44

Slide 44

Every page is entitled to a title.

Slide 45

Slide 45

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

Slide 46

Slide 46

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

Slide 47

Slide 47

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

Slide 48

Slide 48

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

Slide 49

Slide 49

Spinners gonna spin.

Slide 50

Slide 50

Slide 51

Slide 51

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

Slide 52

Slide 52

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

Slide 53

Slide 53

Slide 54

Slide 54

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

Slide 55

Slide 55

<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>

Slide 56

Slide 56

/* 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; }

Slide 57

Slide 57

Slide 58

Slide 58

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

Slide 59

Slide 59

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

Slide 60

Slide 60

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

Slide 61

Slide 61

Thank you.