Workers of the World, Unite!

Gleb Bahmutov, PhD

March 8, 2018 @ 16:00
Mont-Royal

@bahmutov   @confooca   #confoo

C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional

JavaScript ninja, image processing expert, software quality fanatic, Microsoft MVP

@bahmutov   @confooca   #confoo

Cypress.io open source E2E test runner

@bahmutov   @confooca   #confoo

WebWorkers

for Speed

@bahmutov   @confooca   #confoo

60 fps? Cannot do much for each frame

Page runs in a single thread - everything is competing for 1/60 of a second

@bahmutov   @confooca   #confoo

Single thread cannot compute AND paint!

Solution: multiple threads

@bahmutov   @confooca   #confoo

Finding primes demo

@bahmutov   @confooca   #confoo

const worker = new Worker('worker.js')
worker.onmessage = function (e) {
  renderPrimes(e.data);
}
worker.postMessage({ 
  cmd: 'primes', n
})

from "index.html"

@bahmutov   @confooca   #confoo

importScripts('primes.js')
// does this.findPrimes = ...
onmessage = function (e) {
  console.log('worker received message:', 
    e.data)
  switch (e.data.cmd) {
    case 'primes':
      var found = findPrimes(e.data.n)
      self.postMessage(found)
    break
  }
}

in WebWorker

@bahmutov   @confooca   #confoo

worker.postMessage({ 
  cmd: 'primes', n
})

Serialization overhead

worker.postMessage({
  pixels: pixelData.data.buffer,
  width: canvas.width,
  height: canvas.height,
  channels: 4
}, [pixelData.data.buffer])

Transferable objects

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

  • measure total time
  • run N cores
navigator.hardwareConcurrency

Create WebWorker on a fly

@bahmutov   @confooca   #confoo

import greenlet from 'greenlet'

let getName = greenlet( async username => {
    let url = `https://api.github.com/users/${username}`
    let res = await fetch(url)
    let profile = await res.json()
    return profile.name
})

console.log(await getName('developit'))

@bahmutov   @confooca   #confoo

import greenlet from 'greenlet'

const username = 'developit'
let getName = greenlet( async () => {
    let url = `https://api.github.com/users/${username}`
    let res = await fetch(url)
    let profile = await res.json()
    return profile.name
})

console.log(await getName())

* function cannot use external context

@bahmutov   @confooca   #confoo

// app.js
import Worker from './worker.js'
const w = new Worker()

@bahmutov   @confooca   #confoo

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /worker\.js$/,
        use: {
          loader: 'worker-loader',
          options: { 
            publicPath: '/dist/' 
          }
        }
      }
    ]}}

Bundle WebWorker using WebPack

@bahmutov   @confooca   #confoo

What can Web Workers do?

  • JavaScript
  • JSON
  • IndexedDB
  • AJAX
  • ...but not DOM 😢
  • Virtual DOM!

@bahmutov   @confooca   #confoo

Source: http://www.pocketjavascript.com/blog/2015/11/23/introducing-pokedex-org

60fps application even on a mediocre Android phone

React with Web Workers

@bahmutov   @confooca   #confoo

Angular 2 with Web Workers

@bahmutov   @confooca   #confoo

Good learning resource

Feather app

by Henrik Joreteg

@bahmutov   @confooca   #confoo

  • Virtual-dom rendering in WebWorker
  • Simple routing
  • <9KB min + gzip!

@bahmutov   @confooca   #confoo

// main.js
const worker = new WorkerThread()
const rootElement = document.body.firstChild
worker.onmessage = ({data}) => {
  const { payload } = data
  requestAnimationFrame(() => {
    applyPatch(rootElement, payload)
  })
})

@bahmutov   @confooca   #confoo

document.body.addEventListener('click', (event) => {
  const click = event.target['data-click']
  if (click) {
    event.preventDefault()
    worker.postMessage(click)
  }
})
// worker.js
const state = {
  count: 0
}
self.onmessage = ({data}) => {
  const { type, payload } = data
  switch (type) {
    case 'increment': {
      state.count++
      break
    }
    case 'decrement': {
      state.count--
      break
    }
  }

@bahmutov   @confooca   #confoo

  const newVDom = app(state)
  const patches = diff(currentVDom, newVDom)
  currentVDom = newVDom
  self.postMessage({
    payload: serializePatch(patches)
  })
}

ServiceWorker

Blurring the line between client and server

@bahmutov   @confooca   #confoo

If a tree falls while you are in the forest ...

@bahmutov   @confooca   #confoo

<html manifest="example.appcache">
  ...
</html>

Application Cache

CACHE MANIFEST
# v1 2011-08-14
index.html
style.css
image1.png
# Use from network if available
NETWORK:
network.html
# Fallback content
FALLBACK:
/ fallback.html

declarative list

Application Cache

Turns out declaring caching strategy is hard.

@bahmutov   @confooca   #confoo

ServiceWorker

Server

browser

Web Workers

ServiceWorker

Transforms

the response

Transforms

the request

@bahmutov   @confooca   #confoo

Server

browser

service

worker

No changes to the page 😊

@bahmutov   @confooca   #confoo

browser web worker service worker
window self self
localStorage IndexedDB, Cache API (async) IndexedDB, Cache API (async)
XMLHttpRequest, fetch XMLHttpRequest,
fetch
fetch
document

Environment

Promises, postMessage

@bahmutov   @confooca   #confoo

Load ServiceWorker

navigator.serviceWorker.register(
    'app/my-service-worker.js')

Must be https

@bahmutov   @confooca   #confoo

Inside ServiceWorker

self.addEventListener('install', ...)
self.addEventListener('activate', ...)
self.addEventListener('message', ...)
self.addEventListener('push', ...)
self.addEventListener('fetch', function (event) {
  console.log(event.request.url)
  event.respondWith(...)
})
// Cache API

@bahmutov   @confooca   #confoo

Offline support using ServiceWorker

@bahmutov   @confooca   #confoo

self.addEventListener('install', e => {
  const urls = ['/', 'app.css', 'app.js']
  e.waitUntil(
    caches.open('my-app')
      .then(cache => 
        cache.addAll(urls))
  )
})

Cache resources on SW install

self.addEventListener('fetch', e => {
  e.respondWith(
    caches.open('my-app')
      .then(cache =>
        cache => match(e.request))
  )
})

@bahmutov   @confooca   #confoo

Return cached resource

const cacheResources = async () => {
  const urlsToCache = ['/', 'app.css']
  const cache = await caches.open('demo')
  return cache.addAll(urlsToCache)
}
self.addEventListener('install', event =>
  event.waitUntil(cacheResources())
)

Async / await in SW

const cachedResource = async req => {
  const cache = await caches.open('demo')
  return await cache.match(req)
}
self.addEventListener('fetch', event =>
  event.respondWith(cachedResource(event.request))
)

Return cached resource

@bahmutov   @confooca   #confoo

  • Cache only

  • Cache with network fall back

  • Cache then network refresh

  • Network only

  • Network with cache fallback

Caching strategies

@bahmutov   @confooca   #confoo

Fetch event

browser

ServiceWorker

Request

Response

Server

@bahmutov   @confooca   #confoo

Server

browser

ServiceWorker

Request

Response

express.js

http.ClientRequest

JavaScript

http.ServerResponse

JavaScript

Fetch event

@bahmutov   @confooca   #confoo

browser

ServiceWorker

Server

express.js

Server-Side Rendering

@bahmutov   @confooca   #confoo

Server

browser

ServiceWorker

express.js

http.ClientRequest(Request)

http.ServerResponse(Response)

Offline SSR-like Web Apps 

@bahmutov   @confooca   #confoo

Express-service

@bahmutov   @confooca   #confoo

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" 
  content="script-src 'none';">
</head>
<body>
  <form ...>
    <li><form ...></li>
  </form>
</body>
</html>

@bahmutov   @confooca   #confoo

Static SSR page without JavaScript

@bahmutov   @confooca   #confoo

Add an item = form POST

@bahmutov   @confooca   #confoo

Change an item = form PATCH

Server inside ServiceWorker

@bahmutov   @confooca   #confoo

Almost there!!!

@bahmutov   @confooca   #confoo

Next / Nuxt / Sapper

Preload views using SW

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

Watch what happens on hover

1. Service Worker Prerender

2. Sent website as a ZIP file

3. Offline analytics

@bahmutov   @confooca   #confoo

What if an attacker can load malicious ServiceWorker script?

@bahmutov   @confooca   #confoo

// returned JavaScript file
// from same domain
myFunc()
navigator.serviceWorker.register(
  'jsonp?callback=myFunc'
)

Example: unfiltered JSONP endpoint

dynamic text to code

navigator.serviceWorker.register will try to load this code as SW

@bahmutov   @confooca   #confoo

// returned SW script
my malicious SW JS

Example: XSS + unfiltered JSONP endpoint

navigator.serviceWorker.register(
  'jsonp?callback="my malicious SW JS"'
)

navigator.serviceWorker.register will try to load this code as SW :(

@bahmutov   @confooca   #confoo

Malicious ServiceWorker injected via XSS can be really hard to get rid of

Please protect yourself from XSS

@bahmutov   @confooca   #confoo

WebWorkers (Nolan Lawson) + ServiceWorker at NYC Performance Meetup

@bahmutov   @confooca   #confoo

Key Take Aways

Use WebWorkers to offload computation into separate thread

@bahmutov   @confooca   #confoo

Key Take Aways

Add offline support with ServiceWorker

@bahmutov   @confooca   #confoo

Use responsibly

Thank you 👏

Gleb Bahmutov, PhD