tldr;
ES6 generators enable iteration with concise and readable code. However, this convenience comes at a cost.
The example
Let’s say we want to implement a general-purpose flatMap()
function for iterables with the following signature:
function flatMap<T, U>(
items: Iterable<T>,
mapper: (item: T) => Iterable<U>
): Iterable<U>
Let’s implement flatMap()
using both generators and iterators, then compare their performance.
Generators
The generator-based implementation is clean and concise — there’s little room for errors!
function *flatMapGen<T, U>(
items: Iterable<T>,
mapper: (item: T) => Iterable<U>
): Iterable<U> {
for (const item of items) {
yield* mapper(item);
}
}
Iterators
The implementation is a bit more complex, requiring the reader to take a moment to fully grasp it:
function flatMapItr<T, U>(
items: Iterable<T>,
mapper: (item: T) => Iterable<U>
): Iterable<U> {
return {
[Symbol.iterator]() {
const outer = items[Symbol.iterator]();
let inner: Iterator<U>;
return {
next() {
for ( ; ; ) {
if (inner) {
const i = inner.next();
if (!i.done) return i;
}
const o = outer.next();
if (o.done) {
return {
done: true,
value: undefined,
};
}
inner = mapper(o.value)[Symbol.iterator]();
}
}
};
}
}
}
Speed Test
Let's write a benchmark:
console.log('| n | Generators | Iterators | Winner |')
console.log('|---| -----------| ----------|--------|');
[1, 10, 100, 1000, 10000, 100000].forEach(num => {
const input = makeInput(num)
const genOpS = measureOpS(iterNumber => consume(flatMapGen(input, i => ([i + 1, i + iterNumber]))))
const itrOpS = measureOpS(iterNumber => consume(flatMapItr(input, i => ([i + 2, i + iterNumber]))))
const winnerStr = genOpS > itrOpS
? `Generators are ${ roundToOneDigit(genOpS / itrOpS) }x faster`
: `Iterators are ${ roundToOneDigit(itrOpS / genOpS) }x faster`
console.log(`| ${fmt(num)} | ${fmt(genOpS)} | ${fmt(itrOpS)} | ${winnerStr} |`)
})
3
function makeInput(n: number) {
const a = []
for (let i = 0; i < n; i++) a[i] = i * Math.random()
return a
}
function consume(itr: Iterable<number>) {
let x = 0
for (const i of itr) x += i
if (x > 1e12) console.log('Never happens')
}
function measureOpS(op: (iterNumber: number) => unknown) {
// Warm-up...
// Measure...
}
function fmt(num: number) { /* ... */ }
function roundToOneDigit(num: number) { /* ... */ }
See the full code in the repo.
Results
See the full results here.
Node, Chrome, and Edge
These platforms run on V8 or its variants, showing similar performance:
- With an array size of 1, generators are about 2.0x faster.
- With an array size of 10, iterators are slightly faster, by about 1.1x to 1.5x.
- As the array size grows, iterators become 1.7x to 2.0x faster.
Firefox
Iterators are consistently 2.0x to 3.5x faster.
Safari
Iterators are consistently 1.3x to 1.4x faster.
Why are generators slower despite doing the same thing?
Unlike iterators, which are simple objects containing state and closures, generators are suspended functions. Similar to threads in C++ or Java, they maintain their own execution stack. However, unlike threads, generators do not run in parallel with the main thread. Instead, the interpreter starts or resumes a generator’s execution on next()
and switches back to the main thread on yield
. This concept is sometimes referred to as a “coroutine”, though the term is not widely used in JavaScript.
It’s noticeable, though, that creating a generator (i.e., forking the current stack) is highly optimized in V8 — it’s even cheaper than creating an iterator object. However, as the input size grows, the overhead of switching between stacks becomes the dominant factor, negatively impacting the performance of generators.
Conclusion: should I use generators?
If using generators makes your code simpler and easier to understand, go for it! Readable and maintainable code is always a priority, as it can be optimized later if needed.
However, for straightforward tasks like flatMap()
, library implementations, or performance-critical routines, simple iterators remain the preferred choice.
Top comments (2)
very helpful article! (If you received another notification from me on this article, never mind, I did stupid)
Happy you liked it