Multi-threading in JavaScript with Web Workers and WebSocket

Exploring Web Workers and WebSockets: A Simple Introduction to Multi-threading in JavaScript

·

4 min read

Why you should use Multi-Threading ?

As we know, JavaScript is a single-threaded, synchronous language. When implementing a WebSocket on the main thread, it can lead to performance issues, especially if the WebSocket is handling heavy computations. This can result in a noticeable dip in UI performance, causing delays in rendering components.

To avoid these issues, it’s a good practice to offload WebSocket management to a separate thread. This ensures that the main thread is free to handle UI updates and other critical tasks, improving overall performance and responsiveness.

How to create Thread ?

In JavaScript, you can create a new thread using the Worker constructor. The Worker allows you to run JavaScript code in the background, off the main thread.

To assign a task to the worker, you pass the path to a JavaScript file (the worker script) as an argument.

Example:

index.js

// main thread (index.js)
const worker = new Worker('worker.js');

In this example, worker.js is a separate JavaScript file containing the code that will be executed in the worker thread. The main thread can then communicate with the worker via message.

worker.js

to communicate with the main thread we have to use the postMessage and onmessage methods.

// worker thread (worker.js)
let socket = null;

// listen for messages from the main thread
onmessage = (event) => {
  const data = event.data;

  if (data.type === 'connect') {
    // create WebSocket connection
    socket = new WebSocket(data.url);

    // set up WebSocket event listeners
    socket.onopen = () => {
      console.log('WebSocket connected');
      postMessage('WebSocket connected');
    };

    socket.onmessage = (messageEvent) => {
      postMessage(`Received from server: ${messageEvent.data}`);
    };

    socket.onerror = (errorEvent) => {
      postMessage(`WebSocket error: ${errorEvent}`);
    };

    socket.onclose = () => {
      console.log('WebSocket closed');
      postMessage('WebSocket closed');
    };
  }

  if (data.type === 'sendMessage' && socket) {
    // send a message through the WebSocket
    socket.send(data.message);
    postMessage(`Sent message: ${data.message}`);
  }
};

in this file, we’re establishing a Socket connection using WebSocket and communicating with main Thread using postMessage and onmessage methods

index.js

// main thread (index.js)
const worker = new Worker('worker.js');

// listen for messages from the Web Worker
worker.onmessage = (event) => {
  console.log('Received from worker:', event.data);
};

// send some data to the Web Worker
worker.postMessage({ type: 'sendMessage', message: 'Hello from main thread!' });

receiving data from Worker thread by using the same onmessage method and sending back data to Worker Thread using postMessage method

Advantages of Web Workers:

  1. Offload Heavy Tasks:
    Using worker threads allows you to offload heavy tasks, such as WebSocket communication or complex calculations, from the main thread. This ensures the main thread can focus on essential tasks like UI rendering, resulting in a smoother user experience without performance degradation.

  2. Non-Blocking Operations:
    Worker threads handle asynchronous tasks, such as network requests or file I/O, without blocking the main thread. This ensures that the application remains responsive, allowing the UI to continue processing user inputs while background tasks are being executed in parallel.

  3. Better Scalability:
    Worker threads help your application scale more effectively by distributing tasks across multiple threads. As the workload increases, you can spin up additional workers to handle different operations simultaneously, improving performance and maintaining efficiency even as the app grows.

Limitations of Web Workers :

  1. No Direct DOM Access:

    Web workers run on a separate thread and do not have direct access to the DOM. This means you cannot manipulate the UI or interact with DOM elements directly from within the worker. Any updates to the UI must be done by sending messages back to the main thread.

  2. Message-Based Communication:
    Communication between the worker and the main thread happens through messages (using postMessage). While this allows for separation of concerns, it can be slower and less flexible than direct function calls, especially when frequent data exchange is needed

  3. Serialization of Data:
    Data passed between the main thread and the worker is serialized before being sent. This means complex objects, particularly large or nested ones, may need to be copied, leading to potential performance overhead and increased memory usage.

There is always room for improvement:

SharedArrayBuffer and Atomics:

SharedArrayBuffer allows you to create shared memory that can be accessed by multiple threads (like workers),

Atomics is a set of low-level operations for working with shared memory, making it possible to synchronize access to shared data

// main thread (index.js)
const buffer = new SharedArrayBuffer(1024); // create shared memory buffer
const view = new Int32Array(buffer); // typed array to access the buffer
const worker = new Worker('worker.js');

worker.postMessage(buffer); // send the shared memory to the worker

// worker thread (worker.js)
onmessage = function(event) {
  const sharedBuffer = event.data;
  const sharedArray = new Int32Array(sharedBuffer);
  Atomics.store(sharedArray, 0, 42); // update shared memory
  postMessage('Shared memory updated');
};

this approach can be useful if we need multiple threads (workers) to share and modify the same data, this approach can improve performance.

End Note:

there is still advance low level concepts like WebAssembly Threads that can be used in computationally heavy tasks like image processing, simulations, or machine learning where performance is critical

also there is one type of Web Worker called Service Worker (serviceWorker) that allows you to run JavaScript in the background of the application, which is primarily used for task like caching requests, push notifications, offline functionality