Thursday, 31 July 2025
Timi, my pure JavaScript B-Tree, achieves best in class iteration speed in part because I replaced Iterators with callbacks.
They might be convenient, but the design of JavaScript Iterators is inherently slow for complex iteration as it prevents your compiler from inlining code.
Inlining is when the call site of a function is replaced with its body to avoid the overhead of a function call. So, this:
Becomes this:
Inlining can have a dramatic effect on performance. So it's worth paying attention to. When performance tuning, you can use the --trace-turbo-inlining flag to ask TurboFan (one of V8's optimising compilers) to report when it inlines a function.
TurboFan and other optimising compilers can be encouraged to inline calls to your function by keeping it small and simple (and calling it many times). But in this post, I want to consider when inlining fails.
I'm going to deliberately write a function that won't be inlined:
I've padded the add() function from earlier with useless code to prevent TurboFan inlining it. I wasn't scientific about the process, I just typed until it stopped inlining.
Of course, normally, inlining is prevented not by pointless padding but by necessary work. Iteration in Timi, for example, can involve complicated walks up and down the tree and the code is - necessarily - too complex to be inlined.
But why is this a problem for Iterators? Consider the following (I've omitted some imaginary complicated code for clarity):
Which can be used with a for...of loop:
It's a nice API but it hides a function call that I will expand. In reality, the loop executes like this:
It makes a slow non-inlined call to next() every time around the loop. This is unavoidable with an Iterator. But it can be avoided with callbacks.
Now, the same iteration converted to callbacks:
Here, the loop is moved into the iterator and is therefore already inline with the complicated code. If the callback is simple, it too can be inlined leaving no function calls inside the loop.
Here's how it looks in use:
The effect of this optimisation is evident in benchmarks. Below, I've used the padding code I typed into add() as my 'complicated code' block and benchmarked each style using Deno.bench():
The Iterator is 4 times slower than the callback API. If I remove the padding code, the Iterator's next() method can be inlined and the performance gap narrows significantly:
So very simple iterators are often performant enough. Just be careful when your iteration is more complex.
The inversion of control provided by Iterators, generators, and Promises is powerful. But beware any of these in a hot code path. Manually converting these patterns to callbacks might uncover ways to improve performance.
.png)


