Comparing Node.js Asynchronous Alternatives
The Sample Functions
fibonacci folders each contain a sample function implemented in six different ways:
- async: using the
- asyncawait: using this
- bluebird: using the
- callbacks: using plain callbacks.
- co: using the
colibrary (requires node >= 0.11.2 with the
- synchronous: using plain blocking code (just for comparison).
This gives a good indication of the trade-offs between the different coding styles. For the remainer of this document, we'll focus on the most complex sample function, the
largest() sample function is designed to be of moderate complexity, like a real-world problem.
largest(dir, options) finds the largest file in the given directory, optionally performing a recursive search.
dir is the path of the directory to search.
options, if provided, is a hash with the following two keys, both optional:
boolean, defaults to
false): if true,
largest()will recursively search all subdirectories.
boolean, defaults to
false): if true,
largest()will include a small preview of the largest file's content in it's results.
The requirements of
largest() may be summarised as:
- Find the largest file in the given directory (recursively searching subdirectories if the option is selected).
- Keep track of how many files/directories have been processed.
- Get a preview of the file contents (first several characters) if the option is selected.
- Return the details about the largest file and the number of files/directories searched.
- Exploit concurrency wherever possible.
- Don't block Node's event loop anywhere.
The last two requirements are obviously violated by the 'synchronous' variant, but it is worth including for comparison.
Metrics for Comparison
Some interesting metrics with which to compare the six variants are:
- Lines of code (SLOC): Shorter code that does the same thing is usually a good thing.
- Levels of Indenting: Each indent represents a context-shift and therefore higher complexity.
- Anachrony: Asynchronous code may execute in an order very different from its visual representation, which may make it harder to read and reason about in some cases.
- Speed: Node.js is built for speed and throughput, so any loss of speed imposed by a variant may count against it
The following metrics are for the
largest() example function:
|Variant||SLOC ||Indents ||Anachrony ||Ops/sec |
 Includes only lines in the function body; excludes blank lines and comment lines.
 Maximum indentation from the outermost statements in the function body.
 Count of times in the function body when visually lower statements execute before visually higher statements due to asynchronous callbacks.
 Scaled (callbacks = 100), higher is better. Using benchmark.js on my laptop. All benchmarks run in Node v0.10.25 except for
co - see  below.
co benchmark run in Node v0.11.12 with the
 Not strictly comparable because it blocks Node's event loop.
The following observations are based on the above results and obviously may differ substantially with other code and/or on other machines. YMMV. Having said that, at least in this case:
- Plain callbacks are the speed king.
- All other asynchronous variants achieve at least 65% of the speed of plain callbacks.
- Bluebird achieves almost 90% of plain callback speed, living up to its reputation of being extremely well optimised.
asyncawaitis third-fastest in this benchmark, achieving almost 80% of the performance of plain callbacks.
- The source code of
synchronousare virtually identical, with purely mechanical syntax differences.
asyncawait, each using different coroutine technology, are very similar on these metrics. In a choice between these two, the biggest deciding factor may be whether you can use ES6.
- The synchronous approach is actually the slowest, which perhaps makes sense since it can't exploit concurrency.
asynclooks relatively unfavourable compared to the other asynchronous options on these metrics.