Goroutines vs. Node.js Worker Threads: A Performance Showdown
Written on
Chapter 1: Introduction to Parallelism in Node.js and Go
In recent discussions, the node:worker_threads module has gained attention for its ability to execute JavaScript threads in parallel, allowing Node.js to handle CPU-heavy JavaScript tasks without blocking the event loop. This capability is crucial for managing asynchronous operations and event handling effectively.
To explore this further, I conducted a series of tests to compare the performance of Node.js worker threads against Go’s goroutines when executing CPU-intensive tasks.
Section 1.1: Node.js Worker Threads in Action
The Node.js implementation uses the following setup:
// main.js
import { Worker } from 'node:worker_threads';
let workers = [];
let timeStarted = Date.now();
let terminatedWorkers = 0;
for (let i = 0; i < 4; i++) {
let worker = new Worker('./worker.js');
worker.postMessage(i * 10);
worker.on('message', (result) => {
console.log(Received result from worker: ${result});
worker.terminate().then(() => {
terminatedWorkers++;
if (terminatedWorkers === workers.length) {
let timeEnded = Date.now();
console.log('Duration: ' + (timeEnded - timeStarted) + 'ms');
}
});
});
workers.push(worker);
}
The worker file is structured as follows:
// worker.js
import { parentPort } from 'node:worker_threads';
parentPort.on('message', (data) => {
let result = fibonacci(data);
parentPort.postMessage(result);
});
function fibonacci(n) {
if (n <= 1) {
return n;} else {
return fibonacci(n - 1) + fibonacci(n - 2);}
}
When executed multiple times, the average duration for the Node.js worker threads was around 33.25 milliseconds.
Section 1.2: Go's Goroutines for the Same Task
Switching to Go, the equivalent implementation takes advantage of channels and goroutines:
// main.go
package main
import (
"fmt"
"time"
)
func fibonacci(n int) int {
if n <= 1 {
return n}
return fibonacci(n-1) + fibonacci(n-2)
}
func worker(n int, c chan int) {
result := fibonacci(n)
c <- result
}
func main() {
c := make(chan int)
defer close(c)
timeStarted := time.Now()
for i := 0; i < 4; i++ {
go worker(i*10, c)}
for i := 0; i < 4; i++ {
result := <-c
fmt.Printf("Received result from worker: %dn", result)
}
timeEnded := time.Now()
fmt.Printf("Duration: %vn", timeEnded.Sub(timeStarted))
}
Upon running the Go program multiple times, the average duration dropped to approximately 4.03 milliseconds, showcasing a significant performance advantage.
Chapter 2: Performance Insights and Considerations
The video titled "Go Goroutine vs Thread" provides an in-depth analysis of how these two paradigms stack up against each other, offering insights into their performance and efficiency.
The results are clear: Go's goroutines outperform Node.js worker threads in CPU-intensive tasks, achieving speeds over 8 times faster. While this experiment wasn't conducted in a controlled lab environment, it reflects practical performance metrics.
Section 2.1: The Case for Node.js
Despite its slower performance, Node.js boasts a vast ecosystem filled with libraries for virtually every need. If your team is well-versed in JavaScript and your existing codebase is built around Node.js, it may be wise to continue leveraging its strengths despite the performance trade-offs.
Section 2.2: Embracing Go
On the other hand, if speed is your priority and you're willing to explore Go, you might discover that it offers significant advantages. Transitioning to a new language can be challenging, but the potential benefits may outweigh the initial hurdles.
In conclusion, the choice between Go and Node.js ultimately depends on your specific requirements and existing knowledge base. Whether you opt for the speed of Go or the familiarity of Node.js, make an informed decision that aligns with your project goals.