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 full-size slide

  2. web vs native

    View full-size slide

  3. • 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 full-size slide

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

    View full-size slide

  5. We can do better!

    View full-size slide

  6. /developer-blog

    View full-size slide

  7. How it works

    View full-size slide

  8. Prototype built in < 1 day

    View full-size slide

  9. 1. What is a service worker?

    View full-size slide

  10. 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 full-size slide

  11. www.theguardian.com
    https://

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  14. 2. Prime the cache

    View full-size slide

  15. 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 full-size slide

  16. 3. Handle requests with fetch

    View full-size slide

  17. 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 full-size slide

  18. • 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 full-size slide

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

    View full-size slide

  20. Network first, then cache

    View full-size slide

  21. 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 full-size slide

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

    View full-size slide

  23. 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 full-size slide

  24. Cache first, then network

    View full-size slide

  25. 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 full-size slide

  26. 4. Updating the crossword

    View full-size slide

  27. 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 full-size slide

  28. 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 full-size slide

  29. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  32. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  35. 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 full-size slide

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

    View full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

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

    View full-size slide