Browser Is The New ... Server

Gleb Bahmutov

VP of Engineering

April 21st, 2020

Cypress.io

@bahmutov

Q&A and polls at 👉

our planet is in imminent danger

https://lizkeogh.com/2019/07/02/off-the-charts/

+3 degrees Celsius will be the end.

we have to act today

ME

  • home on green electricity
  • ebike everywhere
  • ➡ online conferences instead of flying ⬅

we have to act today

US

survival is possible. but we need to act

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

Me: JavaScript ninja, image processing expert, software quality fanatic

Cambridge, MA

Cypress.io open source E2E test runner

Today's Presentation

  • How Service Workers (SW) work

  • Examples:

    • Resource caching and offline

    • Page prefetching, testing using SW

    • Instant hydration for dynamic apps

    • Server in SW

  • Q&A and Polls: Sli.do event code #new-server

Q&A Sli.do event code #new-server

Poll: Which Worker technology are you familiar with?

ServiceWorker

Blurring the line between client and server

If signal falls while you are in the forest ...

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

ServiceWorker

Server

browser

Web Workers

ServiceWorker

Transforms

the response

Transforms

the request

Server

browser

service

worker

No changes to the page 😊

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

Load ServiceWorker

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

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

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

  • Cache only

  • Cache with network fall back

  • Cache then network refresh

  • Network only

  • Network with cache fallback

Caching strategies

Use

Next / Nuxt / Sapper

Preload views using SW

Example:

Watch what happens on hover

What's Happening

Server

browser

ServiceWorker

hover

prefetch

next page

(cached)

What's Happening

Server

browser

ServiceWorker

hover

prefetch

next page

(cached)

click

fetch

What's Happening

Server

browser

ServiceWorker

hover

prefetch

next page

(cached)

click

fetch

What's Happening

Server

browser

ServiceWorker

hover

prefetch

next page

(cached)

click

fetch

cached

What's Happening

Server

browser

ServiceWorker

hover

prefetch

next page

(cached)

click

fetch

cached

The blue Δ ms gives you extra time to fetch = faster page loads

Network call mocking

Example:

<!-- installs Turtle SW -->
<script src="turtle.js"></script>
<script>
  turtle.get('/some/url', { code: 502, timeout: 3000 });
</script>
// mocks.js
// 1. Import mocking utils
import { composeMocks, rest } from 'msw'

// 2. Define request handlers and response resolvers
const { start } = composeMocks(
  rest.get('https://github.com/octocat', (req, res, ctx) => {
    return res(
      ctx.delay(1500),
      ctx.status(202, 'Mocked status'),
      ctx.json({
        message: 'This is a mocked error',
      }),
    )
  }),
)

// 3. Start the Service Worker
start()

// src/index.js (your application)
if (process.env.NODE_ENV === 'development') {
  require('./mocks')
}

Instant Web Apps

Example:

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

Every time user

changes the page:

On page reload:

Save data + HTML in localStorage

(Brilliant) Idea 💡

Load HTML QUICKLY from localStorage

show static HTML while bootstrapping web app ...

Instant Page Example

Hydration

Single small JS library bahmutov/hydrate-app

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

Works with any web framework

// hydrate-app.js
document.getElementById('app').innerHTML = 
  localStorage.getItem('saved-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">
</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>

Real web app renders into hidden DOM element

HTML

hydrate.js loads cached "app" HTML

app

HTML

data

loads

loads

renders

App says "I am ready!"

Live web app content replaces static cached HTML snapshot

HTML

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

app

HTML

data

loads

loads

renders

App says "I am ready!"

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 (sometimes)

<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

Full page reloads on every action are SLOW 👎

Rendering static page on reload is FAST 👍

If only we could combine client-side speed with full-page rendering on load!

Example: Instant Client-Side Apps

Server

browser

ServiceWorker

<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

Example: Instant Client-Side Apps

Normal client changes

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

Example: Instant Client-Side Apps

Page reload

Example: Instant Client-Side Apps

Q&A Sli.do event code #new-server

Poll: Based on what you have seen, what do you want to use SW for??

Fetch event

browser

ServiceWorker

Request

Response

Server

Server

browser

ServiceWorker

Request

Response

express.js

http.ClientRequest

JavaScript

http.ServerResponse

JavaScript

Fetch event

browser

ServiceWorker

Server

express.js

Server-Side Rendering

Server

browser

ServiceWorker

express.js

http.ClientRequest(Request)

http.ServerResponse(Response)

Offline SSR-like Web Apps 

Express-service

Web application thinks it is interacting with an external server that renders full pages. Yet the server is in ServiceWorker

1. Sending Push events to the site, image or video transcoding, code instrumentation and rewriting on the fly

2. Sent website as a ZIP file

3. Offline analytics

What else?

What if an attacker can load malicious ServiceWorker script?

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

Please protect yourself from XSS

Q&A Sli.do event code #new-server

Poll: What do you think about ServiceWorkers after this presentation? ⭐️: don't care, ⭐️⭐️⭐️⭐️⭐️: WOW!

Key Take Aways

Add offline support with ServiceWorker

Key Take Aways

Go crazy

  • prefetch
  • rewrite pages
  • run server in SW

Use responsibly

WebWorkers (Nolan Lawson) + ServiceWorker at NYC Performance Meetup

Try: Cloudflare Workers

like ServiceWorker but outside the  browser: https://blog.cloudflare.com/introducing-cloudflare-workers/

👏 Thank you 👏

Gleb Bahmutov

VP of Engineering

Cypress.io

@bahmutov