DEV Community

Rigal Patel
Rigal Patel

Posted on

High-Performance JavaScript Simplified: Web Workers, SharedArrayBuffer, and Atomics

JavaScript is a single-threaded language, meaning tasks execute one at a time on the main thread. While this design simplifies development, it can lead to performance bottlenecks for computationally heavy tasks. This blog explores how Web Workers, SharedArrayBuffer, and Atomics can enable multithreading in JavaScript to build high-performance applications.

Why Use Web Workers, SharedArrayBuffer, and Atomics?

Web Workers

Web Workers run JavaScript in background threads, preventing intensive tasks from blocking user interactions like scrolling or button clicks.

SharedArrayBuffer

SharedArrayBuffer allows memory to be shared between the main thread and workers without copying, enabling faster communication.

Atomics

Atomics ensure safe and synchronized access to shared memory, preventing race conditions and maintaining data consistency across threads.

Example: A Real-World Task with Web Workers and SharedArrayBuffer

Let’s implement a simple and real-world example: calculating the sum of a large array in parallel.

Step 1: Creating a Web Worker Script

Create a file named worker.js to handle partial sum calculations:

// worker.js
self.onmessage = function (event) {
    const { array, start, end } = event.data;
    let sum = 0;
    for (let i = start; i < end; i++) {
        sum += array[i];
    }
    self.postMessage(sum);
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Setting Up the Main Thread

In the main script, divide the task among workers.

// main.js
const array = Array.from({ length: 1_000_000 }, () => Math.floor(Math.random() * 100));
const numWorkers = 4;
const chunkSize = Math.ceil(array.length / numWorkers);
const workers = [];
const results = [];
let completedWorkers = 0;

// Create a SharedArrayBuffer for the array
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * array.length);
const sharedArray = new Int32Array(sharedBuffer);
sharedArray.set(array);

// Initialize workers
for (let i = 0; i < numWorkers; i++) {
    const worker = new Worker('worker.js');
    workers.push(worker);

    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, array.length);

    worker.postMessage({ array: sharedArray, start, end });

    worker.onmessage = function (event) {
        results[i] = event.data;
        completedWorkers++;

        if (completedWorkers === numWorkers) {
            const totalSum = results.reduce((acc, curr) => acc + curr, 0);
            console.log('Total Sum:', totalSum);
        }
    };
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Using Atomics for Synchronization

Use Atomics to manage progress or ensure all threads are done before proceeding.

const progress = new Int32Array(sharedBuffer);
Atomics.add(progress, 0, 1); // Increment progress

if (Atomics.load(progress, 0) === numWorkers) {
    console.log('All workers completed their tasks.');
}
Enter fullscreen mode Exit fullscreen mode

Benefits of This Approach

  • Smooth User Experience: Offloads computation from the main thread.

  • Faster Communication: SharedArrayBuffer avoids data copying between threads.

  • Thread Safety: Atomics provide tools to handle synchronization effectively.

Real-World Use Cases

  • Real-Time Analytics: Process large datasets in parallel for faster insights.

  • Gaming Engines: Perform physics simulations in separate threads.

  • Media Processing: Encode or decode video streams without UI lag.

References

MDN Web Docs: Web Workers

MDN Web Docs: SharedArrayBuffer

MDN Web Docs: Atomics

Top comments (0)