This blog is based on O’Reilly’s Mulithreaded Javascript book.

Understanding the Fundamentals: Concurrency vs. Parallelism

Before diving into workers, it is essential to distinguish between how tasks are managed:

source: https://medium.com/swift-india/concurrency-parallelism-threads-processes-async-and-sync-related-39fd951bc61d
  • Concurrency: Executing multiple tasks over overlapping time, though not necessarily at the exact same moment. In a single thread, this looks like tasks taking turns.

  • Parallelism: This is concurrency in a multi-threaded environment where tasks run simultaneously.

source: https://medium.com/swift-india/concurrency-parallelism-threads-processes-async-and-sync-related-39fd951bc61d
  • The Synchronous Model: Each task must wait for the previous one to complete before starting.

  • The Asynchronous Model: Tasks can start without waiting for others to finish. However, it is a common misconception that async code makes a single thread “faster”—it simply prevents blocking.

Is JavaScript Really Single-Threaded?

Technically, yes. JavaScript itself lacks built-in functionality to create threads or even make system calls for networking and filesystems. Even setTimeout() is not a native JavaScript feature. Instead, these capabilities are provided by the Runtime Environment (like Node.js or Browsers) via specific APIs.


Web Workers

Web Workers allow you to spawn new environments to execute JavaScript in, effectively enabling multithreading. Communication between the main thread and workers is strictly event-driven.

1. Dedicated Workers

These are used to parallelize computationally intensive tasks to keep the UI responsive.

Code example: https://github.com/Corbe30/multithreaded-js/tree/main/01-browsers/01-dedicated-worker

  • Note: None of the workers have access to the document (DOM) but can use localStorage and indexedDB.

2. Shared Workers

Unlike dedicated workers, a Shared Worker can be accessed by multiple tabs from the same origin.

Code example: https://github.com/Corbe30/multithreaded-js/tree/main/01-browsers/02-shared-worker

  • Stay Alive: They persist until the very last tab is closed.

  • Use Case: Managing a single Server-Sent Events (SSE) or WebSocket connection to save network traffic across multiple open tabs.

3. Service Workers

Service workers act as a proxy between your app and the network.

Code example: https://github.com/Corbe30/multithreaded-js/tree/main/01-browsers/03-service-worker

  • Persistence: They can exist even after all tabs are closed.

  • Use Case: Enabling offline functionality, caching static assets, and handling push notifications.


Promisifying Workers

When working with Web Workers, the native communication is strictly event-driven, using postMessage() to send data and onmessage to receive it. Promisifying these workers wraps this message-passing logic into a Promise structure, making the asynchronous code cleaner and easier to read.

Note that combining workers and promises does not provide any benefit in terms of execution speed. It’s just a ‘syntacting sugar’.

Code example: https://github.com/Corbe30/multithreaded-js/tree/main/01-browsers/04-promises-worker/1p-4w


Structured Clone Algorithm

It turns out that both Chrome and Safari defer running StructuredDeserialize() until you actually access the .data property on the MessageEvent.

You can think of StructuredSerialize() and StructuredDeserialize() as smarter versions of JSON.stringify() and JSON.parse(), respectively. They are smarter in the sense that they handle cyclical data structures, built-in data types like Map, Set, etc.


CPU vs. GPU

FeatureWeb WorkersGPU.js / GPGPU
Resource usedCPUGPU
WhenGood for parallelismExcellent for massive parallelism
Best forLong, sequential tasksIdentical, independent operations on large datasets
GoalMaintain UI responsiveness by offloading workAchieve maximum calculation throughput for suitable problems

You can view the entire code and presentation here! https://github.com/Corbe30/multithreaded-js