DEV Community

Aleksei Berezkin
Aleksei Berezkin

Posted on • Edited on

ES6 generators vs iterators performance

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>
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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]();
                    }
                }
            };
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

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.

Happy coding!

Top comments (2)

Collapse
 
beqa profile image
beqa

very helpful article! (If you received another notification from me on this article, never mind, I did stupid)

Collapse
 
alekseiberezkin profile image
Aleksei Berezkin

Happy you liked it