Buttery Smooth Web UI with Off-Main-Thread Architecture

github: @mvasigh

twitter: @mehdi_vasigh

linkedin: @mehdi-vasigh

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

1000 ms / 60 frames = 16 ms/frame

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"

www.youtube.com/watch?v=cCOL7MC4Pl0

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:

https://dassur.ma/things/when-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

  1. Profile your application and identify blocking code
  2. 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

  1. If using "on the fly" Object URL method, Workers can't use variables outside of their function scope
  2. If wanting to use dependencies, must integrate into build system
  3. Clean up resources after use
  1. If using "on the fly" Object URL method, Workers can't use variables outside of their function scope
  2. If wanting to use dependencies, must integrate into build system
  3. 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":

  1. Actors are isolated, maintain private internal state, no shared memory (ideal with Workers)
  2. Communicate with other actors via async messages
  3. Can spawn other actors and send them messages

 

Example

PROXX: github.com/GoogleChromeLabs/proxx

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"

web.dev/off-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"

www.youtube.com/watch?v=Kz_zKXiNGSE&

Thank you!

Questions? Ask me here or on Twitter!

github: @mvasigh

twitter: @mehdi_vasigh

linkedin: @mehdi-vasigh

Buttery Smooth Web UI with Off-Main-Thread Architecture

By Mehdi Vasigh

Buttery Smooth Web UI with Off-Main-Thread Architecture

  • 209