{Adventures in Rendering Off the Main Thread}

Simon MacDonald

@macdonst@mastodon.online

I ❤️

🤷🏾

Why do we care about the main thread?

# CHAPTER 1

Main Thread Responsibilities

Parses HTML

Builds the DOM

Parses CSS

Executes JavaScript

Responds to User Events

60 fps

16.7 ms

State of the Art

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>My App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
    <noscript>Bad luck / uflaks</noscript>
  </body>
</html>

🤔

But what if JavaScript fails?

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>My App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
    <noscript>Bad luck / uflaks</noscript>
  </body>
</html>

😏

JavaScript never fails, right?

1-2%

Failure Rate

Failure Reasons

Did the HTTP request for your JS fail?

Was the JS blocked by the corporate firewall?

Is something interfering with your JS?

Has the user switched off JS?

Is data saver mode turned on?

Does a browser plugin mess with your JS?

Ad blocker?

Old Browser?

Page Loads JavaScript Failures
100 1-2
1,000 10-20
10,000 100-200
100,000 1,000-2,000
1,000,000 10,000-20,000

Failure Rates

     some code

😰

Adventure

Reduce

Reduce your JavaScript footprint by server side rendering HTML.

1.

# CHAPTER 2

1

The server sends a "ready to be rendered" HTML response to the browser 

2

The browser renders the HTML page which is now interactive for the user

3

The browser requests JavaScript code from the server

4

Our JavaScript is parsed and our page is progressively enhanced to be even more interactive

  • Time to First Interaction is fast
  • Better SEO
  • Works on older devices and slow network connections

SSR Benefits

Photo by Rayson Tan on Unsplash

Isn't SSR slow 🐢?

No

🤓

The first lesson in rendering off the main thread is to leverage your server to do the initial render

Adventure

Reduce

Reduce your JavaScript footprint by server side rendering HTML

1.

2.

Reuse

Compose our application with re-usable components

# CHAPTER 3

🙋🏻

Can I still use a component framework when using SSR?

Yes

But…

Why use a framework when Web Components exist?

  • Custom Elements ➡️ define components
  • Shadow DOM ➡️ encapsulate styles
  • Slots and Templates ➡️ re-use

Hello World Web Component

const template = document.createElement('template');
template.innerHTML = `
  <h1>
    Hello <slot name="name"></slot>
  </h1>`;


class HelloWorld extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(template.content.cloneNode(true));
  }
}
    
customElements.define('hello-world', HelloWorld);

Hello World Web Component

<hello-world>
  <span slot="name">NDC</slot>
</hello-world>
<hello-world>
  <span slot="name">Oslo</slot>
</hello-world>
 

Hello NDC

Hello Oslo

💁🏿‍♀️

Aren't you contradicting yourself? Web components need JavaScript, right?

😅

WHAT IS ENHANCE?

Author

Enhance allows developers to write components as pure functions that return HTML. Then render them on the server to deliver complete HTML immediately available to the end user.

Standards

Enhance takes care of the tedious parts, allowing you to use today’s Web Platform standards more efficiently. As new standards and platform features become generally available, Enhance will make way for them.

Progressive

Enhance allows for easy progressive enhancement so that working HTML can be further developed to add additional functionality with JavaScript.

Enhance is a web standards-based HTML framework. It’s designed to provide a dependable foundation for building lightweight, flexible, and future-proof web applications.

🤓

The second lesson in rendering off the main thread is to reuse what the browser already provides

Adventure

Reduce

Reduce your JavaScript footprint by server side rendering HTML

1.

2.

Reuse

Use the platform to provide a reusable component architecture with Web Components

3.

Recycle Offload

Expensive or time consuming tasks to web workers

# CHAPTER 4

Web Workers

Web Workers makes it possible to run a script operation in a background thread separate from the main execution thread of a web application.

Main Thread

Web Worker

onclick Event

The user clicks a button to save a new todo

postMessage()

postMessage()

onMessage Event
Sends new todo to our server and awaits the response

onMessage Event
Update our Store and emit an event for the web components to update

Todo API

// Todo API
let worker
export default function saveTodo(todo) {
  if (!worker) {
    worker = new Worker('worker.mjs')
    worker.onmessage = mutate
  }
  
  worker.postMessage({ 
    data: todo
  })
}

const store = Store()
function mutate(result) {
  const copy = Array.from(store.todos)
  copy.push(result)
  store.todos = copy
}

Web Worker

// worker.js
self.onmessage = async function message({ data }) {
  try {
    const result = await (await fetch(
      `/todos/${data.key}`, {
          body: payload,
          credentials: 'same-origin',
          headers: {
            'Content-Type': 'application/json'
          },
          method: 'POST'
        }
    )).json()

    self.postMessage(result)
  }
  catch (err) {
    console.error(err)
  }
}

Todo API

// Todo API
let worker
export default function saveTodo(todo) {
  if (!worker) {
    worker = new Worker('worker.mjs')
    worker.onmessage = mutate
  }
  
  worker.postMessage({ 
    data: todo
  })
}

const store = Store()
function mutate(result) {
  const copy = Array.from(store.todos)
  copy.push(result)
  store.todos = copy
}

Store API

// store.js
function subscribe (fn, props) {
  return listeners.push(fn)
}
function unsubscribe (fn) {
  return listeners.splice(listeners.indexOf(fn), 1)
}
_state.subscribe = subscribe
_state.unsubscribe = unsubscribe
function notify () {
  listeners.forEach(fn => {
    fn(payload)
  })
}
const handler = {
  set: function (obj, prop, value) {
    if (obj[prop] !== value) {
      obj[prop] = value
      set(notify)
    }
    return true
  }
}
const store = new Proxy(_state, handler)

Todo List Web Component

// todo-list.js
class TodosList extends HTMLElement {
  constructor () {
    super()
    this.api = API()
    this.update = this.update.bind(this)
  }

  connectedCallback () {
    this.api.subscribe(this.update, [ 'todos' ])
  }

  disconnectedCallback () {
    this.api.unsubscribe(this.update)
  }

  update ({ todos }) {
    const activeTodos = todos.filter(t => !t.completed)
    const completedTodos = todos.filter(t => t.completed)
    this.activeTodosList.todos = activeTodos
    this.completedTodosList.todos = completedTodos
  }
}

🤓

The third lesson in rendering off the main thread is to use web workers for expensive operations after you've mastered the first two lessons!

In conclusion…

Reduce: the amount of JavaScript required to build your HTML

Reuse: what the platform already provides

Offload: expensive or time consuming tasks to web workers

Use JavaScript

Not a lot

Mostly for progressive enhancement

h/t Michael Pollan

Thanks!

NDC Oslo: Adventures in Rendering Off the Main Thread

By Simon MacDonald

NDC Oslo: Adventures in Rendering Off the Main Thread

  • 687