Workers* of the world, unite!

* Web and Service Workers

Gleb Bahmutov

Kensho

Boston and NYC and hiring!

I love solving AngularJS performance problems

Web App performance has 2 parts

  1. Initial bootstrap time (cold or hot)
  2. Running

Web App performance has 2 parts

  1. Initial bootstrap time (cold or hot)
  2. Running

Simple(r)

Web App performance has 2 parts

  1. Initial bootstrap time (cold or hot)
  2. Running

WebWorkers

Running app perf

Don't do too much

Aside: mad props to Nolan

Web Apps

Q: How to start them quickly?

Using CDN, parallel downloads, caching, small images, above the fold, etc.

Ready, set, go!

HTML

JS

CSS

Ready, set, go!

HTML

JS

CSS

Framework

Ready, set, go!

HTML

JS

CSS

Framework

app

Ready, set, go!

HTML

JS

CSS

Framework

app

get data

Ready, set, go!

HTML

JS

CSS

Framework

app

draw

get data

Ready, set, go!

HTML

JS

CSS

Framework

app

draw

get data

HTML

Hosting / ops

App author

JS

CSS

Framework

app

draw

get data

HTML

Cached by the browser

You should cache

JS

CSS

Framework

app

draw

get data

HTML

This part can be expensive!

Delay while web app starts

AngularJS 1 startup flame chart

ReactJS startup flame chart

Delay while web app starts

PivotalTracker reload

My observations

The data does not change between the reloads*

I do not interact with the page in the first 500ms

Why am I waiting?

nice

what?

If your JS file can overwhelm DevTools - you are doing it wrong.

2.6MB .js file

single 2.6MB .js file

548 "exports" inside

jQuery + 12 plugins

Backbone

React

...

I am not mad, just disappointed

No brainer: cache user data

Even more frustrating

Initial HTML loads

Application code loads

<html>
  <script src="app.js"></script>
  <body>
    <h1>My awesome app</h1>
    <div id="app"></div>
    <footer>...</footer>
  </body>
</html>

Even more frustrating

- App loads data

- App updates page

<html>
  <script src="app.js"></script>
  <body>
    <h1>My awesome app</h1>
    <div id="app"></div>
    <footer>...</footer>
  </body>
</html>

Even more frustrating

- App loads data

- App updates page

Sudden change in the page!

<html>
  <script src="app.js"></script>
  <body>
    <h1>My awesome app</h1>
    <div id="app">
      <ul>
        <li>Clean my room</li>
        <li>Learn Italian</li>
        <li>Finish the slides</li>
      </ul>
    </div>
    <footer>...</footer>
  </body>
</html>

Example

Rendered view is in the middle of the page

source: glebbahmutov.com/hydrate-vue-todo/ (no hydration)

app

HTML

data

load

load

render

App startup

Cache

app

HTML

data

load

load

render

Same data => same HTML

Cache

Load # N

- App loads data

- App updates page

Load # N + 1

Load HTML QUICKLY from localStorage

- load normally ...

Save data + HTML in localStorage

idea!

Hydration

Hydration

Single small JS library bahmutov/hydrate-app

<div id="app">
  ...
</div>
<script src="hydrate-app.js" id="app"></script>

Works with any web framework

// hydrate-app gives you window.bottle
bottle.drink()  // app has loaded and ready to take over
bottle.refill() // save DOM for next reload

app

HTML

data

load

load

render

<div id="app">
  <ul>
    <li>Clean my room</li>
    <li>Learn Italian</li>
    <li>Finish the slides</li>
  </ul>
</div>
<div id="real-app" class="hidden">
</div>

hydrate.js loads "app" HTML

HTML

app

HTML

data

load

load

render

<div id="app">
  <ul>
    <li>Clean my room</li>
    <li>Learn Italian</li>
    <li>Finish the slides</li>
  </ul>
</div>
<div id="real-app" class="hidden">
  <ul>
    <li>Clean my room</li>
    ...
  </ul>
</div>

hydrate.js loads "app" HTML

Web app renders into hidden DOM element

HTML

app

HTML

data

load

load

render

<div id="app">
  <ul>
    <li>Clean my room</li>
    ...
  </ul>
</div>

bottle.drink()

Static cached HTML content replaced with live web app content

Progressive web app for $0.05

HTML

More info

My website: glebbahmutov.com

Hydrate library: bahmutov/hydrate-app

Blog post with examples: Hydrate your apps

Not good enough

There is a flash

<body>
  <header>...</header>
  <div id="app"></div>
  <script src="hydrate-app.js"></script>
</body>
const html = localStorage.get ...
$('#app').innerHTML = '...'

Self-rewriting page is never going to be as smooth as server-side rendering

The Incredibles - at the principal's office

Server-side rendering

The page HTML arrives ready to be rendered by the browser

The browsers are REALLY REALLY good at rendering static pages

If only there was a way to give browser full page from a web app

ServiceWorker (finally)

Server

browser

webworkers

ServiceWorker

Transforms

the response

Transforms

the request

Smart cache(s)

Image / video transcoding

Offline support

Background data sync

Load ServiceWorker

navigator.serviceWorker.register('app/my-service-worker.js', 
    { scope: '/proxied' })

Chrome, Opera

Firefox behind a flag

https!

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
browser web worker service worker
window self self
localStorage IndexedDB, Cache API (async) IndexedDB, Cache API (async)
XMLHttpRequest, fetch XMLHttpRequest,
fetch
fetch
document

Environment and API

Promises, postMessage

Bottle-service

Server

browser

ServiceWorker

<html>
<body>
  <header>...</header>
  <div id="app"></div>
  <footer>...</footer>
</body>
</html>
<html>
<body>
  <header>...</header>
  <div id="app">
   <ul>
     <li>Clean my room</li>
     ...
   </ul>
  </div>
  <footer>...</footer>
</body>
</html>
<ul>
  <li>Clean my room</li>
  ...
</ul>

Cache

Inserts cached HTML fragment into response body HTML

Bottle-service

self.addEventListener('fetch', function (event) {
  event.respondWith(
    fetch(event.request)
      .then(function (response) {
        // merge HTML
        var responseOptions = {
          status: 200,
          headers: {
            'Content-Type': 'text/html charset=UTF-8'
          }
        }
        return new Response(resultHtml, responseOptions)
      })
  )
})

Bottle-service

In/out objects

browser

ServiceWorker

Request

Response

Server

In/out objects

browser

ServiceWorker

Request

Response

http.ClientRequest

http.ServerResponse

Server

express.js

Input => output

Server

browser

ServiceWorker

Request

Response

express.js

http.ClientRequest

JavaScript

http.ServerResponse

JavaScript

browser

ServiceWorker

Server

express.js

Voilà

Server

browser

ServiceWorker

express.js

http.ClientRequest(Request)

http.ServerResponse(Response)

express.js (and middleware)

ServiceWorker env

browserify

var express = require('express')
var app = express()
var url = require('url')
self.addEventListener('fetch', function (event) {
  const parsedUrl = url.parse(event.request.url)
  var req = {
    url: parsedUrl.href,
    method: event.request.method,
    body: body,
    headers: {
      'content-type': event.request.headers.get('content-type')
    },
    unpipe: function () {}
  }
  var res = { /* our mock Node http Server Response object */ }
  event.respondWith(new Promise(function (resolve) {
    app(req, res)
  }))
})

Express-service

"app" is a regular Express.js application

Express-service

Express-service

There is no client-side* JavaScript :)

<link rel="serviceworker" scope="/" href="/sw.js">

*after first ServiceWorker registration

Where is the "Web + Service workers, unite!" part?

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

Environment and API

Promises, postMessage, same JS engine

Babel-service

Server

browser

ServiceWorker

// full ES2015 bundle
const foo = ({a, b=2}) 
    => a + b
var foo = function foo(_ref) {
  var a = _ref.a;
  var _ref$b = _ref.b;
  var b = _ref$b === undefined 
    ? 2 : _ref$b;
  return a + b;
};

worker.js

Test ES2015 feature support and transpile

worker.js

JS feature detection
Babel transpiler

const foo = ({a, b=2}) 
    => a + b

Chrome Canary

Babel-service

People to follow

Workers* of the world, unite!

* Web and Service Workers

Thank you!

slides.com/bahmutov

@bahmutov

glebbahmutov.com

Workers of the world, unite!

By Gleb Bahmutov

Workers of the world, unite!

ServiceWorkers for faster web application boot up. Instant web applications, self-rewriting apps, profiling, ES2015 and modern browsers. Presented at the Web Performance NYC meetup. Video https://youtu.be/4axZ3D75Llg

  • 6,701