Avoid Synchronous Functions in Node.js
Sync Functions can dramatically slow down your app and they are almost never necessary.
tl;dr
Most synchronous methods in Node.js (statSync
, readFileSync
, etc.) have asynchronous alternatives (via callback). Using util.promisify
can easily make these methods async/await
compatible. Using the Promise
-based equivalents can greatly speed up your application. In fact, I'd argue there's never a need for the synchronous versions.
Avoid Synchronous Functions in Node.js
This is a pretty obvious conclusion, and at this point, probably not very controversial either. However, I found myself writing a command-line script the other day and I lazily reached for the synchronous methods (thinking they might save me time). My script would check for the existence of Git repositories, and if they weren't present, I would clone them into a specific directory.
I didn't need this to be fast.
This is what kept telling myself. That was true until I started cloning over 100 repositories. This process took many, many minutes -- and I was still iterating on the script. This means my feedback cycle was incredibly long for minor changes.
To be fair to myself, I knew exactly where my performance bottleneck was. But this got me thinking...
Why did I reach for synchronous functions in the first place?
I think this is because when we are just trying to write something quickly, we try to avoid introducing async
scaffolding because of the boilerplate involved (since we lack top-level async
in a script file). It's also annoying to add the tiny bit of boilerplate around promisify
ing functions.
But when we look at the difference in code, there's not much of a difference.
Synchronous:
import { readFileSync } from 'fs'
const contents = readFileSync(path)
console.log(contents)
Asynchronous:
import { readFile } from 'fs'
import { promisify } from 'util'
const readFileAsync = promisify(readFile)
async function main() {
const contents = await readFileAsync(path)
console.log(contents)
}
main()
Where we get ourselves into trouble is that we think the time savings with the synchronous functions are actually going to pay off in the end.
I've found the opposite to be true. In fact, it's pretty painful to convert a mostly synchronous application into an async one after the fact.
There are also subtle concurrency problems people may not realize when they use synchronous functions:
import { readFileSync } from 'fs'
const [file1, file2, file3, file4] = await Promise.all(
['file1.txt', 'file2.txt', 'file3.txt', 'file4.txt']
.map(async (file) => {
return readFileSync(file)
})
)
This is a contrived example, but it demonstrates what can happen when you've mixed async and sync functions in your codebase. The synchronous functions block the main event loop. This means nothing else happens in your application during that operation!
In the example above, there is no concurrency when what you actually wanted was two or more files are read at the same time. Basically, it's no different than a for loop processing operations sequentially.
Duh, Richard. Why are you being such a prima donna about this?
Well, turns out some of the libraries you use might actually use synchronous functions without you realizing it (https://github.com/motdotla/dotenv/issues/252). The authors may have their reasons, but my point is perhaps we should just commit to never using synchronous methods? At the very least, we are doing ourselves a favor. As a framework/library author, however, we wouldn't be leaving our users an unintended surprise.
Conclusion
I'm struggling to find an occasion where the use of synchronous functions is appropriate. When I found myself using them last night, and had to refactor, I wondered why I had reached for them. This cemented the idea that I should commit to never using them. I encourage others to do the same. This is an artifact of the old Node.js API that hasn't served us well. Perhaps it's time to retire it?
You might also be interested in these articles...
Stumbling my way through the great wastelands of enterprise software development.