Build Snappier Apps

with React and Web Workers

Mehdi Vasigh

twitter: @mehdi_vasigh

Who am I?

Engineer @ Mailchimp

♥️ creative coding

👩‍🍳 UI chef

I love meeting people!

Twitter: @mehdi_vasigh

What does it mean to have a high-performance web UI?

The frame budget

We want to paint 60 frames each second

1000 ms / 60 frames = 16 ms/frame

Browser takes ~4-6 ms per frame to do work

Our JS for each frame should 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

A silly example

Bouncy Ball Fibonacci: web-worker-example.netlify.app

JS is single-threaded*

  • One single "execution context" where all code runs
  • If a function takes long to execute, the thread is blocked

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

  • Browser enables concurrency via the event loop
  • 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

Web Workers allow us to do work in parallel using separate threads

Web Workers

  • Scripts run on a separate thread
  • Have their own execution context
  • Don't block the UI
  • Data is copied, memory is not shared

How do Web Workers... work?

  • Runs JS code in a completely separate execution context
  • Can communicate with main script using postMessage
  • Messages can contain all primitive JS types
  • A few other types (i.e. ArrayBuffer, ImageData) can be transferred
  • 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 have isolated, hot path logic!

When NOT to use

  • Work is I/O bound, not CPU bound
    • i.e. if your constraint is network requests
  • DOM manipulation is being performed
  • localStorage (or some other unavailable API) is being used
  • Prematurely

Let's look at some code!

Ecosystem & Tools: Bundlers

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

(Web Worker support is baked in! Hooray!)

Ecosystem & Tools: Libraries

comlink: github.com/NativeScript/worker-loader

Allows you to create "local instances" of objects living in a Web Worker

and use them as if they're any other object.

 

workerize: github.com/GoogleChromeLabs/worker-plugin

Allows you to move a module into a Web Worker and exposes exported functions as methods

 

useWorker: github.com/surma/rollup-plugin-off-main-thread

Similar to workerize, but as a React Hook

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

Made with Slides.com