Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building an Offline Page for theguardian.com — JSConf Budapest, May 2016

Building an Offline Page for theguardian.com — JSConf Budapest, 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. This talk will explain how Oliver used service workers to build an offline page for theguardian.com.

Oliver Joseph Ash

May 14, 2016
Tweet

More Decks by Oliver Joseph Ash

Other Decks in Technology

Transcript

  1. Building an
    Offline Page for
    theguardian.com
    Oliver Joseph Ash – JSConf Budapest, May 2016

    @OliverJAsh

    View Slide

  2. @OliverJAsh

    View Slide

  3. View Slide

  4. View Slide

  5. web vs native

    View Slide

  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

    View Slide

  7. Experience:
    • offline: nothing
    • server down: nothing
    • poor connection: white screen of death
    • good connection: new content
    Website

    View Slide

  8. View Slide

  9. We can do better!

    View Slide

  10. /developer-blog

    View Slide

  11. How it works

    View Slide

  12. Prototype built in < 1 day

    View Slide

  13. 1. What is a service worker?

    View Slide

  14. View Slide

  15. View Slide

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

    View Slide

  17. www.theguardian.com
    https://

    View Slide

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

    View Slide

  19. <br/>if (navigator.serviceWorker) {<br/>navigator.serviceWorker.register('/service-worker.js');<br/>}<br/>
    1. Create and register the service worker

    View Slide

  20. 1
    2

    View Slide

  21. View Slide

  22. View Slide

  23. 2. Prime the cache

    View Slide

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

    View Slide

  25. 1
    2

    View Slide

  26. 3. Handle requests with fetch

    View Slide

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

    View Slide

  28. View Slide

  29. View Slide

  30. • e.g., use templating to construct
    a HTML response from JSON
    Service worker: custom responses
    self.addEventListener('fetch', function (event) {
    var responseBody =
    'Hello, World!';
    var responseOptions = {
    headers: {
    'Content-Type': 'text/html'
    }
    };
    var response = new Response(
    responseBody,
    responseOptions
    );
    event.respondWith(response);
    });

    View Slide

  31. Mutable (HTML)
    Immutable (assets: CSS, JS)

    View Slide

  32. HTML

    View Slide

  33. Network first, then cache

    View Slide

  34. 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));
    }
    });

    View Slide

  35. self.addEventListener('fetch', function (event) {
    var request = event.request;
    if (doesRequestAcceptHtml(request)) {
    event.respondWith(
    fetch(request).catch(function () {
    return caches.match('/offline-page');
    })
    );
    }
    });

    View Slide

  36. View Slide

  37. Assets

    View Slide

  38. View Slide

  39. 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)
    );
    }
    });

    View Slide

  40. View Slide

  41. Cache first, then network

    View Slide

  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).then(function (response) {
    return response || fetch(request);
    })
    );
    }
    });

    View Slide

  43. 4. Updating the crossword

    View Slide

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

    View Slide

  45. View Slide

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

    View Slide

  47. Offline-first
    • 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

    View Slide

  48. View Slide

  49. View Slide

  50. View Slide

  51. Problems and caveats
    • Browser bugs in both Chrome and Firefox
    • Interleaving of versions in CDN cache

    View Slide

  52. service-worker.js
    v1.css
    offline-page.html
    caches
    caches
    depends on

    View Slide

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

    View Slide

  54. service-worker.js
    v2.css
    offline-page.html
    v2
    v1 v2
    v1.css v1
    caches
    caches
    depends on

    View Slide

  55. // /offline-page.json
    {
    "html": "",
    "assets": ["/v1.css"]
    }
    Solution: cache manifest

    View Slide

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

    View Slide

  57. “If we only use features that work in IE8, we're
    condemning ourselves to live in an IE8 world.”
    — Nolan Lawson

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide