March 8, 2018 @ 16:00
Mont-Royal
C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional
JavaScript ninja, image processing expert, software quality fanatic, Microsoft MVP
Cypress.io open source E2E test runner
Page runs in a single thread - everything is competing for 1/60 of a second
const worker = new Worker('worker.js')
worker.onmessage = function (e) {
renderPrimes(e.data);
}
worker.postMessage({
cmd: 'primes', n
})
from "index.html"
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
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
navigator.hardwareConcurrency
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'))
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
// app.js
import Worker from './worker.js'
const w = new Worker()
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /worker\.js$/,
use: {
loader: 'worker-loader',
options: {
publicPath: '/dist/'
}
}
}
]}}
Bundle WebWorker using WebPack
Source: http://www.pocketjavascript.com/blog/2015/11/23/introducing-pokedex-org
by Henrik Joreteg
// main.js
const worker = new WorkerThread()
const rootElement = document.body.firstChild
worker.onmessage = ({data}) => {
const { payload } = data
requestAnimationFrame(() => {
applyPatch(rootElement, payload)
})
})
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
}
}
const newVDom = app(state)
const patches = diff(currentVDom, newVDom)
currentVDom = newVDom
self.postMessage({
payload: serializePatch(patches)
})
}
If a tree falls while you are in the forest ...
<html manifest="example.appcache">
...
</html>
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
Turns out declaring caching strategy is hard.
ServiceWorker
Server
browser
Web Workers
ServiceWorker
Transforms
the response
Transforms
the request
Server
browser
service
worker
browser | web worker | service worker |
---|---|---|
window | self | self |
localStorage | IndexedDB, Cache API (async) | IndexedDB, Cache API (async) |
XMLHttpRequest, fetch | XMLHttpRequest, fetch |
fetch |
document |
Promises, postMessage
navigator.serviceWorker.register(
'app/my-service-worker.js')
Must be https
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
Offline support using ServiceWorker
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))
)
})
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
browser
ServiceWorker
Request
Response
Server
Server
browser
ServiceWorker
Request
Response
express.js
http.ClientRequest
JavaScript
http.ServerResponse
JavaScript
browser
ServiceWorker
Server
express.js
Server
browser
ServiceWorker
express.js
http.ClientRequest(Request)
http.ServerResponse(Response)
Demo: https://express-service.herokuapp.com/
Video: https://youtu.be/4axZ3D75Llg?t=31m30s
The library: github.com/bahmutov/express-service
Blog post with examples: Run Express server in your browser
<!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>
Static SSR page without JavaScript
Add an item = form POST
Change an item = form PATCH
Server inside ServiceWorker
Preload views using SW
Watch what happens on hover
1. Service Worker Prerender
2. Sent website as a ZIP file
3. Offline analytics
// 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
// 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 :(
Malicious ServiceWorker injected via XSS can be really hard to get rid of
Please protect yourself from XSS
Use WebWorkers to offload computation into separate thread
Add offline support with ServiceWorker
Use responsibly