Mozilla

Offline first

  1. Architecting your web site
  2. The Service Worker Life Cycle
  3. Offline caches
  4. Fetch strategies
  5. Handling updates
  6. Other considerations

Architecting your web site

The app shell pattern

The goal is to reduce time to first meaningful paint.

Find what is content v.s. what is essential UI.

Shell / content separations

The Service Worker Life Cycle

  1. Install
  2. Activate
  3. Fetch

Install


self.addEventListener('install', evt => {
  evt.waitUntil(prefetch());
});

function prefetch() {
  return self.caches.open(CURRENT_CACHE)
         .then(cache => cache.addAll(SHELL_ASSETS));
}
            

Activate


self.addEventListener('activate', evt => {
  evt.waitUntil(clearOldCaches());
});

function clearOldCaches() {
  return self.caches.keys().then(names => {
    return Promise.all(names.map(cacheName => {
      if (cacheName === CURRENT_CACHE) {
        return Promise.resolve();
      }
      else {
        return self.caches.delete(cacheName);
      }
    }));
  });
}
            

Fetch


self.addEventListener('fetch', evt => {
  var req = evt.request;
  if (isShell(req)) {
    evt.respondWith(handleShell(req));
  }
  else {
    evt.respondWith(handleContent(req));
  }
});
            

Offline caches

  1. The new API is called Cache.
  2. Request-Response pairs as records.
  3. Storing a pair request / response consumes the bodies.
  4. Only GET requests can be stored.
  5. Can look up by request or URL.

Fetch strategies

  • Cache-first
  • Cache-only
  • Network-first
  • Network-only
  • Fastest
  • Offline fallback
  • Fetch and update

Cache-first


function cacheFirst(req) {
  return self.caches.match(req).then(res => {
    return res || self.fetch(req);
  });
}
            

Cache-only


function cacheOnly(req) {
  return self.caches.match(req).then(res => {
    return res || Promise.reject('not-found');
  });
}
            

Network-first


function networkFirst(req) {
  return self.fetch(req)
         .then(res => res.ok ? res : cacheOnly(req))
         .catch(() => cacheOnly(req));
}
            

Network-first with timeout


function networkFirstWithTimeout(req, timeout) {
  return new Promise((fulfil, reject) => {
    setTimeout(() => fulfil(cacheOnly(req)), timeout);
    networkFirst(req).then(fulfil, reject);
  });
}
            

Network-only


function networkOnly(req) {
  return self.fetch(req);
}
            

Fastest


function fastest(req) {
  return race(networkOnly(req), cacheOnly);
}

function race(promiseA, promiseB) {
  return new Promise((fulfil, reject) => {
    promiseA.then(fulfil, () => promiseB.catch(reject));
    promiseB.then(fulfil, () => promiseA.catch(reject));
  });
}
            

Offline fallback


function offlineFallback(req) {
  var defaultRequest = getDefault(req, 'by-type');
  return cacheOnly(defaultRequest);
}
            

Fetch and update


self.addEventListener('fetch', evt => {
  var request = evt.request;
  var cacheResponse = cacheOnly(request);
  var networkResponse = networkOnly(request);
  evt.respondWith(race(
    cacheResponse,
    networkResponse.then(response => response.clone())
  ));
  evt.waitUntil(networkResponse.then(response => update(request, response.clone())));
});

function update(request, response) {
  return self.caches.open(CURRENT_CACHE)
         .then(cache => cache.put(request, response));
}
            

Handling updates

  • Rely on Service Worker life cycle
  • Implement your own

Other considerations

  1. What about the http cache?
  2. Should I bump my requests?
  3. How to deal with sensitive information?
  4. Don't abuse offline caches.
  5. Are my assets permanently stored?
  6. Can I cache external resources?
  7. Kill switch.