The largest
, countFiles
, and fibonacci
folders each contain a sample function implemented in six different ways:
- async: using the
async
library. - asyncawait: using this
asyncawait
library. - bluebird: using the
bluebird
library. - callbacks: using plain callbacks.
- co: using the
co
library (requires node >= 0.11.2 with the--harmony
flag). - 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()
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:
recurse
(boolean
, defaults tofalse
): if true,largest()
will recursively search all subdirectories.preview
(boolean
, defaults tofalse
): 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.
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 [1] | Indents [2] | Anachrony [3] | Ops/sec [4] |
---|---|---|---|---|
async | 67 | 7 | 5 | ~65 |
asyncawait | 23 | 2 | - | ~79 |
bluebird | 44 | 3 | 8 | ~89 |
callbacks | 84 | 6 | 9 | ~100 |
co | 23 | 2 | - | ~68 [5] |
synchronous | 23 | 2 | - | ~63 [6] |
[1] Includes only lines in the function body; excludes blank lines and comment lines.
[2] Maximum indentation from the outermost statements in the function body.
[3] Count of times in the function body when visually lower statements execute before visually higher statements due to asynchronous callbacks.
[4] Scaled (callbacks = 100), higher is better. Using benchmark.js on my laptop. All benchmarks run in Node v0.10.25 except for co
- see [5] below.
[5] co
benchmark run in Node v0.11.12 with the --harmony
flag.
[6] 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.
asyncawait
is third-fastest in this benchmark, achieving almost 80% of the performance of plain callbacks.- The source code of
co
,asyncawait
, andsynchronous
are virtually identical, with purely mechanical syntax differences. co
andasyncawait
, 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.
async
looks relatively unfavourable compared to the other asynchronous options on these metrics.