Building an Offline Page for theguardian.com — Front-Trends, May 2016

Building an Offline Page for theguardian.com — Front-Trends, May 2016

You’re on a train to work and you open up the Guardian app on your phone. A tunnel surrounds you, but the app still works in very much the same way as it usually would—despite your lack of internet connection, you still get the full experience, only the content shown will be stale. If you tried the same for the Guardian website, however, it wouldn’t load at all.

Native apps have long had the tools to deal with these situations, in order to deliver rich user experiences whatever the user’s situation may be. With service workers, the web is catching up. In this talk Olly will demonstrate how he built an offline page for http://theguardian.com, and discuss potential future use cases.

There have been lots of talks about service workers in recent times, but rarely have people used them in production and lived to tell the tale. This talk aims to inspire people to start using them in production, and to demonstrate the various caveats they might come across.

D97317c84e6f6bbccb2732b619648c03?s=128

Oliver Joseph Ash

May 20, 2016
Tweet

Transcript

  1. Building an Offline Page for theguardian.com Oliver Joseph Ash –

    Front-Trends, May 2016
 @OliverJAsh
  2. @OliverJAsh

  3. None
  4. None
  5. web vs native

  6. • Content is cached • Experience: • offline: stale content

    remains • server down: stale content remains • poor connection: stale while revalidate • good connection: stale while revalidate Native app
  7. Experience: • offline: nothing • server down: nothing • poor

    connection: white screen of death • good connection: new content Website
  8. None
  9. We can do better!

  10. /developer-blog

  11. How it works

  12. Prototype built in < 1 day

  13. 1

  14. . Create and register the service worker 1

  15. What is a service worker?

  16. None
  17. What is a service worker? • Script that runs in

    the background • Useful for features with no user interaction, e.g.: • listen to push events, useful for pushing notifications • intercept and handle network requests • future: • background sync • alarms (e.g. reminders) • geofencing • A progressive enhancement • Trusted origins only (HTTPS, localhost) • Chrome, Opera, and Firefox stable
  18. www.theguardian.com https://

  19. https://…/info …/science …/technology …/business

  20. <script> if (navigator.serviceWorker) { navigator.serviceWorker.register('/service-worker.js'); } </script>

  21. 1 2

  22. None
  23. None
  24. 2

  25. . Prime the cache 2

  26. var version = 1; var staticCacheName = 'static' + version;

    var updateCache = function () { return caches.open(staticCacheName) .then(function (cache) { return cache.addAll([ '/offline-page', '/assets/css/main.css', '/assets/js/main.js' ]); }); }; self.addEventListener('install', function (event) { event.waitUntil(updateCache()); }); • install event: get ready! • Cache the assets needed later • Version the cache
  27. 1 2

  28. 3

  29. . Handle requests with fetch 3

  30. const staticCacheName = 'static'; const version = 1; const updateCache

    = () => ( caches.open(staticCacheName + version) .then(cache => cache.addAll([ '/offline-page.html', '/assets/css/main.css', '/assets/js/main.js' ]); ); ); self.addEventListener('install', function (event) { event.waitUntil(updateCache()); }); self.addEventListener('fetch', function (event) { event.respondWith(fetch(event.request)); }); • Default: just fetch • Override default • Intercept network requests to: • fetch from the network • read from the cache • construct your own response fetch events
  31. None
  32. None
  33. • e.g., use templating to construct a HTML response from

    JSON Service worker: custom responses self.addEventListener('fetch', function (event) { var responseBody = '<h1>Hello, World!</h1>'; var responseOptions = { headers: { 'Content-Type': 'text/html' } }; var response = new Response( responseBody, responseOptions ); event.respondWith(response); });
  34. Mutable (HTML) Immutable (assets: CSS, JS)

  35. HTML

  36. Network first, then cache

  37. var doesRequestAcceptHtml = function (request) { return request.headers.get('Accept') .split(',') .some(function

    (type) { return type === ‘text/html’; }); }; self.addEventListener('fetch', function (event) { var request = event.request; if (doesRequestAcceptHtml(request)) { event.respondWith(fetch(request)); } });
  38. self.addEventListener('fetch', function (event) { var request = event.request; if (doesRequestAcceptHtml(request))

    { event.respondWith( fetch(request).catch(function () { return caches.match('/offline-page'); }) ); } });
  39. None
  40. Assets

  41. None
  42. self.addEventListener('fetch', function (event) { var request = event.request; if (doesRequestAcceptHtml(request))

    { event.respondWith( fetch(request).catch(function () { return caches.match('/offline-page'); }) ); } else { event.respondWith( caches.match(request) ); } });
  43. None
  44. Cache first, then network

  45. self.addEventListener('fetch', function (event) { var request = event.request; if (doesRequestAcceptHtml(request))

    { event.respondWith( fetch(request).catch(function () { return caches.match('/offline-page'); }) ); } else { event.respondWith( caches.match(request).then(function (response) { return response || fetch(request); }) ); } });
  46. 4

  47. . Updating the crossword 4

  48. var version = 1; var cacheNameSuffix = 'static' + '-'

    + version; var getISODate = function () { return new Date().toISOString().split('T')[0]; }; var getCacheName = function () { return getISODate() + '-' + cacheNameSuffix; }; var isLatestCacheName = function (key) { return key === getCacheName(); }; var deleteOldCaches = function () { var getOldCacheKeys = function () { return caches.keys().then(function (keys) { return keys.filter(function (key) { return !isLatestCacheName(key); }); }); }; return getOldCacheKeys().then(function (oldCacheKeys) { return Promise.all(oldCacheKeys.map(function (key) { return caches.delete(key); })); }); }; var isCacheUpdated = function () { return caches.keys().then(function (keys) { return keys.some(isLatestCacheName); }); }; isCacheUpdated().then(function (isUpdated) { if (!isUpdated) { updateCache().then(deleteOldCaches); } }); // <normal fetch handling code>
  49. None
  50. Future usages of service worker on theguardian.com • Offline page

    will increase in significance • Offline page can be extended • show stale content, like native apps • show personalised content, downloaded ahead of time • show content that has been “saved for later” • Go fully offline-first
  51. Offline-first • Render as much as possible without the network

    • Instantly respond to navigations with a “shell” • Improves the experience for users with poor connections • No more white screen of death • Show stale content whilst fetching new content
  52. None
  53. None
  54. None
  55. None
  56. Problems and caveats • Browser bugs in both Chrome and

    Firefox • Interleaving of versions in CDN cache
  57. service-worker.js v1.css offline-page.html caches caches depends on

  58. 0 60 120 Seconds /v1.css TTL: 1 year /offline-page.html TTL:

    60 seconds /service-worker.js TTL: 60 seconds v1 v2 v1 v2
  59. 0 60 120 Seconds /v1.css TTL: 1 year /v2.css TTL:

    1 year /offline-page.html TTL: 60 seconds /service-worker.js TTL: 60 seconds v1 v2 v1 v2 Deploy
  60. service-worker.js v2.css offline-page.html v2 v1 v2 v1.css v1 caches caches

    depends on
  61. // /offline-page.json { "html": "<html><!-- v1 --></html>", "assets": ["/v1.css"] }

    Solution: cache manifest
  62. Why? Is this valuable? • Fun • Insignificant usage due

    to HTTPS/browser support • … but good to plant the seed • Iron out browser bugs, pushes the web forward
  63. “If we only use features that work in IE8, we're

    condemning ourselves to live in an IE8 world.” — Nolan Lawson
  64. Conclusion • Service workers allow us to progressively enhance the

    experience for: • offline users • users with poor connections • It’s easy to build an offline page • A simple offline page is a good place to start
  65. Further reading • https://www.theguardian.com/info/developer-blog/2015/nov/04/ building-an-offline-page-for-theguardiancom • https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md • https://www.theguardian.com/service-worker.js

  66. Thank you Like what we’re doing? developers.theguardian.com @OliverJAsh

  67. None