Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for NodeJS: 4.8x faster if we go back to callbacks!
Gemma Black
Gemma Black

Posted on • Originally published atgemmablack.dev

NodeJS: 4.8x faster if we go back to callbacks!

Yeah, I said it!

Callbacks are4.8x faster when running them parallel over async/await in parallel. And only1.9x faster when we run sequential callbacks.

I've modified this article somewhat after I got some helpful and kind comments about my dodgy test. 😂🙏

Thank you toRicardo Lopes andRyan Poe for taking the time to steer the benchmarks in the right direction. My first faux pas was I wasn't actually waiting for the execution of the code to finish, which crazily skewed the results. The second was I was comparing parallel to sequential runtimes, which make the benchmarks worthless.

So this is round 2 which addresses my initial errors. Previously, I said:

NodeJS: 34.7x faster if we go back to callbacks! I shouldn't have believed it. 🤣

Not as impressive as my bad benchmarks before (and see comments for context), but still a sizeable difference.

So what exactly did I test?

I compared callbacks to promises and async/await when reading a file, 10,000 times. And maybe that's a silly test but I wanted to know, which is faster at I/O.

Then I finally compared callbacks in Node.js to Go!

Now, guess who won?

I won't be mean.TLDR. Golang!

Lower is better. Results are inms.

Now, true benchmarkers? Go easy on me on this one. But please do leave your comments to make me a better person.

Everyone keeps saying that Node.js is slow!

And it bugs me out.

Because what does slow mean? As with all benchmarks, mine is contextual.

I started reading about theEvent Loop, just to even begin to understandhow it works.

But the main thing I've understood is that Node.js passes I/O tasks onto a queue that sits outside the main Node.js executable thread. This queue runs onpure C. A number of threads could potentially handle these I/O operations. And that's where Node.js can shine, handling I/O.

Promises, however, get handled in the main, single executable thread. And async/await, is well, promises but now with blocking added.

Event loop consisting of 6 different queues.

So are callbacks faster than promises?

Let's put it to the test.

First off. Mymachine! Complements of working withKamma. It's important to note what resources we're working with. Plenty memory and CPU.

MacBook Pro(14-inch, 2021)Chip      Apple M1 ProMemory    32 GBCores     10NodeJS    v20.8.1Go        1.21.0
Enter fullscreen modeExit fullscreen mode

So we have atext.txt file with anoriginal message,Hello, world.

echo"Hello, world"> text.txt
Enter fullscreen modeExit fullscreen mode

And we'll read this text file using native Node.js, which means, zero node module dependencies because we don't want to drag the speed down with the heaviest objects in the universe.

Heaviest Objects In The Universe : r/ProgrammerHumor

Callbacks

Parallel callbacks

First, let's start withparallel callbacks. I'm interested in how quickly the same file can be read as quickly as possible, all at once. And what's faster than parallel?

// > file-callback-parallel.test.mjsimporttestfrom'node:test';importassertfrom'node:assert';importfsfrom"node:fs";test('reading file 10,000 times with callback parallel',(t,done)=>{letcount=0;for(leti=0;i<10000;i++){fs.readFile("./text.txt",{encoding:'utf-8'},(err,data)=>{assert.strictEqual(data,"Hello, world");count++if(count===10000){done()}})}});
Enter fullscreen modeExit fullscreen mode

Sequential callbacks

Second, we have callbacks again, but sequential (or rather blocking). I'm interested in how quickly the same file can be read sequentially. Having not done callbacks calling callbacks for ages, this was fun to try again. Albeit, it doesn't look pretty.

// > file-callback-blocking.test.mjsimporttestfrom'node:test';importassertfrom'node:assert';importfsfrom"node:fs";letread=(i,callback)=>{fs.readFile("./text.txt",{encoding:'utf-8'},(err,data)=>{assert.strictEqual(data,"Hello, world");i+=1if(i===10000){returncallback()}read(i,callback)})}test('reading file 10,000 times with callback blocking',(t,done)=>{read(0,done)});
Enter fullscreen modeExit fullscreen mode

Async/Await

Then we have async/await. My favourite way of working with Nodejs.

Parallel async/await

It's as parallel as I can get with async/await. I load all thereadFile operations into an array and await them all usingPromise.all.

// > file-async-parallel.test.mjsimporttestfrom'node:test';importassertfrom'node:assert';importfsfrom"node:fs/promises";test('reading file 10,000 times with async parallel',async(t)=>{letallFiles=[]for(leti=0;i<10000;i++){allFiles.push(fs.readFile("./text.txt",{encoding:'utf-8'}))}returnawaitPromise.all(allFiles).then(allFiles=>{returnallFiles.forEach((data)=>{assert.strictEqual(data,"Hello, world");})})});
Enter fullscreen modeExit fullscreen mode

Sequential Async/Await

This was the easiest and most concise one to write.

// > file-async-blocking.test.mjsimporttestfrom'node:test';importassertfrom'node:assert';importfsfrom"node:fs/promises";test('reading file 10,000 times with async blocking',async(t)=>{for(leti=0;i<10000;i++){letdata=awaitfs.readFile("./text.txt",{encoding:'utf-8'})assert.strictEqual(data,"Hello, world");}});
Enter fullscreen modeExit fullscreen mode

Promises

Finally, we have promises without async/await. I've long stopped using them in favour ofasync/await but I was interested in whether they were performant or not.

Parallel promises

// > file-promise-parallel.test.mjsimporttestfrom'node:test';importassertfrom'node:assert';importfsfrom"node:fs/promises";test('reading file 10,000 times with promise parallel',(t,done)=>{letallFiles=[]for(leti=0;i<10000;i++){allFiles.push(fs.readFile("./text.txt",{encoding:'utf-8'}))}Promise.all(allFiles).then(allFiles=>{for(leti=0;i<10000;i++){assert.strictEqual(allFiles[i],"Hello, world");}done()})});
Enter fullscreen modeExit fullscreen mode

Sequential promises.

Again, we want to wait for the execution of allreadFile operations.

// > file-promise-blocking.test.mjsimporttestfrom'node:test';importassertfrom'node:assert';importfsfrom"node:fs/promises";test('reading file 10,000 times with promises blocking',(t,done)=>{letcount=0;for(leti=0;i<10000;i++){letdata=fs.readFile("./text.txt",{encoding:'utf-8'}).then(data=>{assert.strictEqual(data,"Hello, world")count++if(count===10000){done()}})}});
Enter fullscreen modeExit fullscreen mode

And voila! Results 🎉! I even ran it a few times to get a better reading.

I ran each test by doing:

node--test <file>.mjs
Enter fullscreen modeExit fullscreen mode

Reading a file 10,000 times with callbacks is over 5.8x faster than with async/await in parallel! It's also 4.7x faster than with promises in parallel!

So, in Node.js land, callbacksare more performant!

Now is Go faster than Node.js?

Well, I don't write in Go, so this may be truly terrible code because I asked ChatGPT to help me and yet, itseems pretty decent.

Hey ho. Let's go. Our Golang code.

packagemainimport("fmt""io/ioutil""time")funcmain(){startTime:=time.Now()fori:=0;i<10000;i++{data,err:=ioutil.ReadFile("./text.txt")iferr!=nil{fmt.Printf("Error reading file: %v\n",err)return}ifstring(data)!="Hello, world"{fmt.Println("File content mismatch: got",string(data),", want Hello, world")return}}duration:=time.Since(startTime)fmt.Printf("Test execution time: %v\n",duration)}
Enter fullscreen modeExit fullscreen mode

And we run it as so:

go run main.go
Enter fullscreen modeExit fullscreen mode

And the results?

Test executiontime: 58.877125ms
Enter fullscreen modeExit fullscreen mode

🤯 Go is 4.9x faster than Node.js using sequential callbacks. Node.js only comes close with parallel execution.

Node.js Async/await is 9.2x slower than Go.

So yes. Node.js is slower. Still, 10,000 files in sub 300ms isn't to be scoffed at. But I've been humbled by Go's speediness!

Now just a side note. Do I have bad benchmarks?

I really did have terrible Benchmarks. Thank you again to Ricardo and Ryan.

Yes, I did. Hopefully now they're better.

But you may ask, who's really going to read the same file, over and over again? But for a relative test between things, I hope it's a helpful comparison.

I also don't know how many threads Node.js is using.

I don't know how my CPU cores affect Go vs Node.js performance.

I could just rent an AWS machine with one core and compare.

Is it because I'm on Mac M1?

How would Node.js perform on a Linux or...Windows? 😱

And there's the practicality of, yes, reading a file is one thing, but at some point, you have to wait anyway for the file to be read to do something with the data in the file. So, speed on the main thread is still pretty important.

Now, do you really want to use callbacks?

I mean, do you really, really want to?

I don't know. I definitely don't want to tell anyone what to do.

But I like the clean syntax of async/awaits.

They look better.

They read better.

I know better is subjective here but I remember callback-hell, and I was grateful when promises came into existence. It made Javascript bearable.

Now, Golang is clearly faster than Node.js at its optimum, with callbacks, and with async/await, by 9.2x! So if we want good readability and performance, Golang is the winner. Although, I'd love to learn how Golang looks under the hood.

Anywho. This was fun. It was more of an exercise to help me understand how callbacks and I/O work in the Event Loop.

So to sign out

Is Node.js slow? Or are we just using Node.js on slow mode?

Probably where performance matters, Golang is worth the jump. I'll certainly be looking more at using Golang in future.


Updates

Sequential promises

If I rewrite it as below, I'm still using callbacks 😬 but at least it's sequential:

importtestfrom'node:test';importassertfrom'node:assert';importfsfrom"node:fs/promises";letread=(i,callback)=>{letdata=fs.readFile("./text.txt",{encoding:'utf-8'}).then(data=>{assert.strictEqual(data,"Hello, world")i+=1if(i===10000){returncallback()}read(i,callback)})}test('reading file 10,000 times with promises blocking',(t,done)=>{read(0,done)});
Enter fullscreen modeExit fullscreen mode

I didn't include the above in the benchmarks but it is slower and not surprisingly so at532.777667ms.

Top comments(3)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
baublet profile image
Ryan M. Poe
  • Joined

You are correct here that callbacks are faster than promises! However, there are problems in your test cases. Your callbacks are being queued up, but your test is exiting (and thus considered "finished") before they have a chance to finish.

There's a different problem with the promises ones, too, but that is also skewing results. Your callback example is not onlynot waiting for completion, they're firing all at once, while your promise examples are firing sequentially! If you parallelize your promises, you get a far more similar result.

Also note thatasync/await and.then are the same API. Async/await is syntactic sugar over.then for more linear, easier-to-follow programming.

callbacks: duration_ms 1434.361861promises: async/await duration_ms 3230.272977promises: .then duration_ms 3102.076352
Enter fullscreen modeExit fullscreen mode

Callbacks code:

importtestfrom"node:test";importassertfrom"node:assert";importfsfrom"node:fs";test("reading file 10,000 times with callback",(t,done)=>{letcallbacksUnfinished=0;for(leti=0;i<10000;i++){callbacksUnfinished++;fs.readFile("./text.txt",{encoding:"utf-8"},(err,data)=>{assert.strictEqual(data,"Hello, world");callbacksUnfinished--;if(callbacksUnfinished===0){done();}});}});
Enter fullscreen modeExit fullscreen mode

async/await promises code:

importtestfrom'node:test';importfsfrom"node:fs/promises";test('reading file 10,000 times with promise',async(t)=>{constpromises=[];for(leti=0;i<10000;i++){promises.push(fs.readFile("./text.txt",{encoding:'utf-8'}));}awaitPromise.all(promises);});
Enter fullscreen modeExit fullscreen mode

And finally,.then promises test code:

importtestfrom"node:test";importassertfrom"node:assert";importfsfrom"node:fs/promises";test("reading file 10,000 times with promise",(t,done)=>{letcallbacksUnfinished=0;for(leti=0;i<10000;i++){callbacksUnfinished++;fs.readFile("./text.txt",{encoding:"utf-8"}).then((data)=>{assert.strictEqual(data,"Hello, world");callbacksUnfinished--;if(callbacksUnfinished===0){done();}});}});
Enter fullscreen modeExit fullscreen mode

And yeah, I agree with your point: Node is not slow. People tend to do very easily-spotted poorly-performant things in code and blame their tools for letting them get themselves into that mess. You don't see roofers blaming their hammers for not securing a roof. There's not a tool on the planet that can save you from having to wait for sequential IO calls.

Node has a very well-knownmajor limitation that shouldn't surprise anyone, anymore -- it's single threaded. But in my experience, I've encountered maybe 3 problems ever that desperately needed multi-threaded capabilities. (Which we tend to fix using workers and tasks to distribute work, anyway...)

CollapseExpand
 
gemmablack profile image
Gemma Black
Fullstacker by day, coder by night. Working on uposcar.com.
  • Location
    Valladolid, Spain | London, UK
  • Education
    King's College University of London
  • Work
    Senior Software Engineer
  • Joined

Thanks for your well-written explanation Ryan. You're so kind to not have completely destroyed me for getting that wrong. So I really appreciate it. And I like your implementation of checking that the call back is finished!

Your callback example is not only not waiting for completion, they're firing all at once, while your promise examples are firing sequentially! If you parallelize your promises, you get a far more similar result.

Yeah, I definitely messed up there.

I rewrote the tests to also compare a parallel set of callbacks and then a blocking set as well, and to my surprise, promises are more performant! So I've probably done something wrong again. I'll update the article too. I hope you won't mind me mentioning you as a reviewer! And thanks again for pointing out the major flaw!

CollapseExpand
 
baublet profile image
Ryan M. Poe
  • Joined

Nice updates! Glad to help, and keep up the good work. Some fun thought provoking stuff here. 👍

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Fullstacker by day, coder by night. Working on uposcar.com.
  • Location
    Valladolid, Spain | London, UK
  • Education
    King's College University of London
  • Work
    Senior Software Engineer
  • Joined

More fromGemma Black

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp