Slide 1
Jeremy Wagner — https://jlwagner.net/ NDC Porto — Porto, Portugal — April 2022
Slide 2
Slide 3
A quick service worker primer.
Slide 4
Slide 5
Slide 6
A tale of two navigation patterns.
Slide 7
Slide 8
Where are you going with this?
Slide 9
Slide 10
Slide 11
Slide 12
The anatomy of a website.
Slide 13
Slide 14
Slide 15
npm install \ workbox-navigation-preload \ workbox-strategies \ workbox-routing \ workbox-precaching \ workbox-streams \ —save
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
// ./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
https://developer.chrome.com/blog/navigation-preload/
Slide 19
Slide 20
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
// ./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
// 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
// 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
// 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
// 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
// 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
<?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
<?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
// 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 32
https://developer.chrome.com/docs/workbox/access-caches-from-the-window/
Slide 33
// Prior code omitted… // … const navigationHandler = composeStrategies([ () => matchPrecache(“/partial-header.php”), ({ event }) => contentStrategy.handle(event), () => matchPrecache(“/partial-footer.php”) ]);
Slide 34
// Prior code omitted… // … registerRoute(({request}) => request.mode === ‘navigate’, navigationHandler);
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 37
No service worker.
With service worker.
Slide 38
fi
What’s the eld data story?
Slide 39
https://developer.chrome.com/blog/navigation-preload/
Slide 40
FCP 2,721 ms ❌
584 ms
90th percentile
Slide 41
LCP 2862 ms ❌
2059 ms
90th percentile
Slide 42
TTFB 1848 ms ❌
187 ms
90th percentile
Slide 43
Slide 44
Every page is entitled to a title.
Slide 45
<title> Contact — jlwagner.net </title>
Slide 46
<title> <?php if ($isPartial === false) { echo $title . “—jlwagner.net”; } ?> </title>
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
<script type=”application/json” id=”page-data”> { “title”: “Contact” } </script>
Slide 49
Slide 50
Slide 51
/* This rule assumes your header partial ends with an open <main> element. */ main:empty { content: “Loading…”; }
Slide 52
/* This rule assumes your header partial ends with an open <main> element. */ main:empty { content: “Loading…”; }
Slide 53
Slide 54
<script> const effectiveType = navigator?.connection?.effectiveType; if (effectiveType !== “4g”) { document.documentElement.classList.add(“slow”); } </script>
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
/* 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 58
https://philipwalton.com/articles/smaller-html-payloads-with-service-workers/
Slide 59
https://developer.chrome.com/blog/beyond-spa/
Slide 60
https://alistapart.com/article/now-thats-what-i-call-service-worker/
Slide 61