Service Worker 101

Working Offline and Instant Loading

@GDG DevFest Beijing Nov 2016

⚡️

{
  "name": ["黄玄", "Hux", "@huxpro"],
  "icons": [{










   }],
  "start_url": "            ",
  "social_network": ["     ", "      ", "    "],
  "work_experience": ["@alibaba", "@wepiao"]
}

Web is constructed with the assumption that you have put yourself into the "internet mode"

⟵          ⟶

Network

HTTP

Download & Run on the fly

 

Working Offline?

 

App Cache

<html manifest="cache.appcache">
CACHE MANIFEST

CACHE:
style/default.css
images/sound-icon.png
images/background.png

NETWORK:
comm.cgi

HTTP caching Race Condition

Unrecoverable Errors (Cache the manifest)

Success only If ALL resources are cached

Not a Progressive Enhancement

Caching Dynamic Contents needs hack & re-architecture

Full of Assumption

The Cache is organized by "Manifest"

Manifest changes trigger ALL cached files redownload.

NO clean up mechanism for cache

Deficit of Flexibility

Unprogrammable

Very Limited Fallback/Routing

 

Instant Loading?

 

⟵          ⟶

Network

HTTP

Round Trip is Expensive

Dependency Graph

⟵          ⟶

Network

HTTP

HTTP Caching

Mobile is HARD due to its flaky network

INSUFFICIENT!

 

⟷          ⟷

↕︎

Service Worker

Cache

Network

Client-side Proxy

SW 101 Live Coding #1

"Hello World" of SW!

"Living Coding"

// register Service Worker in index.html
if('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('/sw.js')
    .then( registration => {
      console.log('Service Worker Registered');
    }).catch( error => {
      console.log('Registration failed with' + error);
    })
}

Register

SW 101 Gotcha #0

HTTPS as Prerequisite

Stay Secure

/* sw.js */
self.onfetch = (e) => {
  e.respondWith(new Response('Hello World from SW!'))
}

Say "Hello World"

😱😱😱

⟷          ⟷

↕︎

Service Worker

Cache

Network

How can SW be started before

navigations occur?

SW 101 Gotcha #1

SW is a Web Worker

[SecureContext, Exposed=(Window,Worker)]
interface ServiceWorker : EventTarget {
  readonly attribute USVString scriptURL;
  readonly attribute ServiceWorkerState state;
  void postMessage(any message, optional sequence<object> transfer = []);

  // event
  attribute EventHandler onstatechange;
};
ServiceWorker implements AbstractWorker;

WD 3.1 ServiceWorker

  • Web Workers

  • SharedWorker 

  • Chrome Background Pages

  • Chrome Event Pages

  • !App Cache

  • Web Workers

  • SharedWorker 

  • Chrome Background Pages

  • Chrome Event Pages

  • !App Cache

  • Entirely Async

  • Shared across browsing context 

  • Persistent Background Processing

  • Event-Driven, Time-Limited

  • Programmable

SW 101 Gotcha #2

be Started and Killed Anytime

↕︎

Register
Install
Error
Terminated
Idle
Activated
Active

↕︎

↕︎

⤵︎

↕︎

Register
Install
Error
Terminated
Idle
Activated
Active

↕︎

↕︎

⤵︎

LifeCycle Events

↕︎

Register
Install
Error
Terminated
Idle
Activated
Active

↕︎

↕︎

⤵︎

Functional Events

Fetch
Push
Sync

 

↕︎

Register
Install
Error
Terminated
Idle
Activated
Active

↕︎

↕︎

⤵︎

Registered => fire "Install" 

↕︎

Register
Install
Error
Terminated
Idle
Activated
Active

↕︎

↕︎

⤵︎

The Real Installability!

// sw.js
self.oninstall = (e) => {
  e.waitUntil(
    caches.open('installation')
      .then(cache =>  cache.addAll([
        '/styles.css',
        '/script.js'
      ]))
  )
});

On Install

😱😱😱

SW 101 Gotcha #3

Scriptable File-Level Install

SW 101 Live Coding #2

Make Your Own Dinosaurs!

self.onfetch = (e) => {
  const fetched = fetch(e.request)
  const cached = caches.match(e.request)
  const sorry = caches.match('offline.html')

  e.respondWith(
    fetched.catch(_ => cached).then(res => res || sorry)
  )
}

Network falling to Cache/Sorry

self.onfetch = (e) => {
  // chrome would disk-cache any res with no 'cache-control',
  // which broke the assumption here we want 
  // fetching index.html failed on offline
  const fetched = fetch(`${e.request.url}?${Date.now()}`)
  // but would also break revalidation depends on your CDN 
  const cached = caches.match(e.request)
  const sorry = caches.match('offline.html')

  e.respondWith(
    fetched.catch(_ => cached).then(res => res || sorry)
  )
}

cache: 'no-cache' workaround  

interface ExtendableEvent : Event {
  void waitUntil(Promise<any> f);
};

WD 4.4 ExtendableEvent

WD 4.6 FetchEvent

interface FetchEvent : ExtendableEvent {
  [SameObject] readonly attribute Request request;
  void respondWith(Promise<Response> r);
};

SW 101 Gotcha #4

SW is for SECOND Load

[Exposed=ServiceWorker]
interface Clients {
  // Jake: take control of uncontrolled clients
  [NewObject] Promise<void> claim();
};

WD 4.3.4 claim()

Override Default (Racing)

SW 101 Live Coding #3

Stale While Revalidate

self.onfetch = (e) => {
  const cached = caches.match(e.request)
  const fallback = caches.match('offline.html')
  const fetched = fetch(`${e.request.url}?${Date.now()}`)


  e.respondWith(
    cached
      .then(res => res || fetched)
      .catch(_ => fallback)
  )
}

Cache-First (Stale) & Fallback

self.onfetch = (e) => {
  const cached = caches.match(e.request)
  const fallback = caches.match('offline.html')
  const fetched = fetch(`${e.request.url}?${Date.now()}`)


  e.respondWith(
    Promise.race([fetched.catch(_ => cached), cached])
      .then(resp => resp || fetched)
      .catch(_ => fallback)
  )
}

Fastest & Fallback

self.onfetch = (e) => {
  const cached = caches.match(e.request)
  const fallback = caches.match('offline.html')
  const fetched = fetch(`${e.request.url}?${Date.now()}`)
  const fetchedCopy = fetched.then(_ => _.clone())

  e.respondWith(
    cached
      .then(res => res || fetched)
      .catch(_ => fallback)
  )

  e.waitUntil(
    Promise.all([fetchedCopy, caches.open('runtime')])
      .then(([resp, cache]) => resp.ok && cache.put(e.request, resp))
      .catch(_ => {/* swallow */})
  )
}

Stale-W-Revalidate & Fallback

self.onfetch = (e) => {
  const cached = caches.match(e.request)
  const fallback = caches.match('offline.html')
  const fetched = fetch(`${e.request.url}?${Date.now()}`)
  const fetchedCopy = fetched.then(_ => _.clone())

  e.respondWith(
    Promise.race([fetched.catch(_ => cached), cached])
      .then(resp => resp || fetched)
      .catch(_ => fallback)
  )

  e.waitUntil(
    Promise.all([fetchedCopy, caches.open('runtime')])
      .then(([resp, cache]) => resp.ok && cache.put(e.request, resp))
      .catch(_ => {/* swallow */})
  )
}

Fastest-W-Revalidate & Fallback

SW 101 Gotcha #5

You are IN CONTROL

WD 4.1.3 SkipWaiting()

interface ServiceWorkerRegistration : EventTarget {
  readonly attribute ServiceWorker? installing;
  readonly attribute ServiceWorker? waiting;
  readonly attribute ServiceWorker? active;
}
self.onactivate = (event) => {
  // delete any caches that aren't in expectedCaches
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(console.log("V2 is ready!");)
  );
});

On Activate

Clean-up & Migration.

SW 101 Gotcha #6

Architectural Revolution

RWD      Media Query        "Mobile First"

PWA      Service Worker    "Offline First"

Ajax       XHR                          "Async First"

Buzzword    Core Technology            Archtechutal Style

Resources & Tools

Visit Google in China

(without bypassing GFW)

SW 101 Gotcha #7

The Belief In Web

Thanks

Q/A