Concurrency and multithreading in JavaScript: how to make code run faster
JavaScript has historically been a single-threaded language, but this does not mean it cannot handle multiple tasks simultaneously. In modern Node.js and browsers, there are powerful mechanisms for working with concurrency and multithreading. Understanding these concepts is critically important for creating high-performance server and client applications.
In this article, we will break down how concurrency differs from parallelism, how the Event Loop works, when to use Worker Threads and Web Workers, and also look at practical examples in Node.js.
1. Event Loop and non-blocking I/O
The foundation of concurrency in JavaScript is the Event Loop. This is a mechanism that allows asynchronous operations (e.g., reading files, database queries) to be executed without blocking the main thread.
Imagine a queue: the Event Loop takes a task from the queue (call stack), executes it, and if it encounters an asynchronous operation (e.g., setTimeout or fetch), it passes it to the Web API (in the browser) or libuv (in Node.js). After the operation completes, its callback is placed into the task queue. The Event Loop constantly checks if the stack is empty, and if it is, it takes the next task from the queue.
Example of asynchronous code:
const fs = require('fs');
console.log('Start');
fs.readFile('example.txt', 'utf8', (err, data) => { if (err) throw err; console.log('File read:', data);});
console.log('End');
// Output:// Start// End// File read: ...Notice: console.log('End') executes before the file is read. This is non-blocking I/O — the main thread does not wait for the operation to complete, but continues execution.
2. Web Workers and Worker Threads: true multithreading
While the Event Loop handles I/O operations well, it does not help with executing "heavy" computations (image processing, cryptography, complex sorting). If you run such code in the main thread, the entire interface (or server) will "freeze" until the computation finishes.
To solve this problem, there are Web Workers (in the browser) and Worker Threads (in Node.js). They allow running JavaScript code in separate operating system threads.
Example with Worker Threads in Node.js
Let's create a file worker.js:
const { parentPort } = require('worker_threads');
// Heavy task: calculating the sum of numberslet sum = 0;for (let i = 0; i < 1e9; i++) { sum += i;}
// Send the result back to the main threadparentPort.postMessage(sum);Main file main.js:
const { Worker } = require('worker_threads');
console.log('Main thread: starting worker');
const worker = new Worker('./worker.js');
worker.on('message', (result) => { console.log('Result from worker:', result);});
worker.on('error', (err) => { console.error('Worker error:', err);});
worker.on('exit', (code) => { if (code !== 0) { console.error(`Worker exited with code ${code}`); }});
console.log('Main thread: continuing work...');
// Output:// Main thread: starting worker// Main thread: continuing work...// Result from worker: 499999999500000000As you can see, the main thread is not blocked, and the worker performs computations in the background. This is true multithreading.
3. Async/Await and Promise: managing concurrency
Modern JavaScript offers elegant ways to work with asynchronous code — async/await and Promise. They do not create new threads, b