Browser is the new server

Dr. Gleb Bahmutov PhD

Work at Kensho

A DATA ANALYTICS AND MACHINE INTELLIGENCE COMPANY

Boston / NYC

I love solving AngularJS performance problems

Web App performance: 2 parts

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

< 16ms, Virtual DOM, WebWorkers

How to quickly start a web app?

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

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!

PivotalTracker HOT reload

why am I waiting?

The data does not change between the reloads*

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

nice

what?

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>

- 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>

Even more frustrating

app

HTML

data

loads

renders

HTML = APP(data)

same

same

app

HTML

data

loads

renders

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

loads

loads

renders

<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 cached "app" HTML

HTML

app

HTML

data

loads

loads

renders

<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>

Web app renders into hidden DOM element

HTML

hydrate.js loads cached "app" HTML

app

HTML

data

loads

loads

renders

bottle.drink()

Live web app content replaces static cached HTML snapshot

Progressive web app for $0.05

HTML

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

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

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 the full page from a web app

ServiceWorker

Server

browser

Web Workers

ServiceWorker

Transforms

the response

Transforms

the request

Smart caching

OFFLINE SUPPORT

Image / video transcoding

Background data sync

Server

browser

service

worker

No changes to the page 😊

Load ServiceWorker

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

Chrome, Opera

Firefox behind a flag

Must be 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

Promises, postMessage

Instant pages using SW: 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

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)

Instant pages using SW: Bottle-service

Instant pages using SW: Bottle-service

If the server goes down in the forest ...

Stop treating browser like a child

Fetch event

browser

ServiceWorker

Request

Response

Server

Input => output

Server

browser

ServiceWorker

Request

Response

express.js

http.ClientRequest

JavaScript

http.ServerResponse

JavaScript

browser

ServiceWorker

Server

express.js

child-like app

Server

browser

ServiceWorker

express.js

http.ClientRequest(Request)

http.ServerResponse(Response)

grown up app using SW

express.js + middleware

ServiceWorker env

browserify

var express = require('express')
var app = express()
var url = require('url')
self.addEventListener('fetch', function (event) {










Express-service

"app" is a regular Express.js application

  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

Express-service

There is no client-side* JavaScript :)

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

*after first ServiceWorker registration

Express-service

bonus

$ npm i -g nativefier
$ nativefier https://express-service.gleb-demos.com/
Packaging app for platform darwin x64 using electron v1.1.3
App built to /Users/gleb/git/ExpressService-darwin-x64

Express-service stand alone desktop app

want to know more?

isomorphic

universal

jumping?

grown up?

independent?

need good name

Browser is the new server

Instant apps

Server in the browser

Code transpiling, coverage, testing