Buttery Smooth Web UI with Off-Main-Thread Architecture
Samuel Taylor Coleridge

Public Domain, https://commons.wikimedia.org/w/index.php?curid=3976299
Suspension of Disbelief
What does it mean to have a high-performance web UI?
The frame budget
Smooth animation means painting 60 frames per second
Browser takes ~4-6 ms per frame to do work
All of our JS for each frame has to take ~10-12 ms or less
Browser pixel pipeline
JavaScript
Style
Layout
Paint
Composite
https://developers.google.com/web/fundamentals/performance/rendering
16 ms
JavaScript
Style
Layout
Paint
Composite
Browser can skip steps if nothing changes
16 ms
JavaScript
Style
Layout
Paint
Composite
16 ms
UI/Local Logic
JavaScript
Style
Layout
Paint
Composite
16 ms
CSS-in-JS Runtime
UI/Local Logic
JavaScript
Style
Layout
Paint
Composite
16 ms
CSS-in-JS Runtime
UI/Local Logic
Front-end Framework
JavaScript
Style
Layout
Paint
Composite
16 ms
CSS-in-JS Runtime
UI/Local Logic
Front-end Framework
State Management Logic
We've exceeded our frame budget and introduced jank
JavaScript
Style
Layout
Paint
Composite
16 ms
The more blocking JavaScript that we have, the more frames that we will drop
JS is single-threaded*
- One single "execution context" where all code runs
- If a function takes long to execute, everything freezes (blocking code)

What does "blocking" mean?
- Code that occupies the thread
- Prevents or delays execution of code after it
while (true) {
console.log('To infinity and beyond!');
}
// Will never run!
extremelyImportantFunction();
JS is async by design
- Event loop allows us to write async, non-blocking code
-
This is why fetch or setTimeout don't block the render
Jake Archibald - "In the Loop"
Concurrency vs. Parallelism
The Event Loop offers a framework for concurrency, and Promises/Tasks offer a good way of handling it in our code

To enable parallelism, we need to offload work from our main UI thread using Web Workers
Web Workers
Run scripts in separate background thread, so that we don't block the main thread
Web Workers
- Runs JS code in a completely separate execution context
- Can communicate with main script using postMessage
- Messages can contain most basic JS data types, but not functions
- Data is copied, not shared (no shared memory)
- Workers can create new Workers
When to use Web Workers:
When to use
- CPU-intensive work
- Working with a lot of data
- Image processing
- Parallelizing tasks
- Frames are being dropped
- Games or lots of animations
- ... anywhere that you don't need `window`!
When NOT to use
- I/O bound, not CPU bound
- i.e. if your constraint is network requests
- DOM manipulation
- Prematurely
// worker.js
function handleMessage(e) {
if (e.data === 'ping...') {
self.postMessage('pong!');
}
}
self.addEventListener('message', handleMessage);
// main.js
const worker = new Worker('worker.js');
function handleMessage(e) {
console.log(e.data);
}
worker.addEventListener('message', handleMessage);
worker.postMessage('ping...');
// Console: pong!
Demo
Build-system Integration
Webpack
worker-loader: github.com/NativeScript/worker-loader
worker-plugin: github.com/GoogleChromeLabs/worker-plugin
Rollup
rollup-plugin-off-main-thread: github.com/surma/rollup-plugin-off-main-thread
Parcel
(Web Worker support is baked in! Hooray!)
Patterns: off-main-thread tasks
- Profile your application and identify blocking code
- Offload blocking code as one-off async tasks using a Web Worker
Depending on the nature of the code, you may not need to alter build system (i.e. using URL.createObjectURL and Blob)
Examples:
- Typeahead/autocomplete search
- Image processing or large file/dataset processing
Patterns: off-main-thread tasks
- If using "on the fly" Object URL method, Workers can't use variables outside of their function scope
- If wanting to use dependencies, must integrate into build system
- Clean up resources after use
workerize: github.com/developit/workerize
comlink: github.com/GoogleChromeLabs/comlink
- If using "on the fly" Object URL method, Workers can't use variables outside of their function scope
- If wanting to use dependencies, must integrate into build system
- Clean up resources after use
Patterns: services/Actor model
The Actor model: www.brianstorti.com/the-actor-model/
Treat different components of your UI system as "actors":
- Actors are isolated, maintain private internal state, no shared memory (ideal with Workers)
- Communicate with other actors via async messages
- Can spawn other actors and send them messages
Example
Additional Information
Mehdi Vasigh - "Getting Started with JavaScript Web Workers and Off-Main-Thread Tasks"
dev.to/mvasigh/getting-started-with-javascript-web-workers-and-off-main-thread-tasks-4029
Surma - "Use web workers to run JavaScript off the browser's main thread"
Surma - "The main thread is overworked and underpaid"
www.youtube.com/watch?v=7Rrv9qFMWNM
Jason Teplitz - "Using Web Workers for more responsive apps"
Thank you!
Questions? Ask me here or on Twitter!
Buttery Smooth Web UI with Off-Main-Thread Architecture
By Mehdi Vasigh
Buttery Smooth Web UI with Off-Main-Thread Architecture
- 209