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