In this animation, I am wondering if it's a good idea using async/await with recursive calls and the effect may be caused to the javascript event loop.

Core function:

const wait = (ms) => new Promise(r => setTimeout(r, ms));

async function typeWriter(text, n, elementId, waitAfter) {
    const el = document.getElementById(elementId);
    if (n < text.length) {
        requestAnimationFrame(() => {
            el.innerHTML = (text.substring(0, n + 1));
        await wait(waitAfter) //wait after letter stroke
        await typeWriter(text, n, elementId, waitAfter) //recursion till text finish

Full code




  • \$\begingroup\$ Please add an explanation of what the code is supposed to do. What is it trying to accomplish? \$\endgroup\$ – Snowbody Jan 17 '18 at 14:39
  • \$\begingroup\$ @snowbody I attached the typewriter result link above \$\endgroup\$ – Microsmsm Jan 17 '18 at 16:47
  • \$\begingroup\$ Yes, I can see that there is a web link, but in order for this qquestion to be considered you need to explain in the question itself what the code is supposed to do. e.g. "It's supposed to animate the characters appearing one at a time with a random delay". \$\endgroup\$ – Snowbody Jan 18 '18 at 5:47

Recursion is a state stack.

Function context

Every time a function is called JS creates a new context (function state), even if the function does nothing, creates no variables, has no arguments, only calls another function, it still requires its own context. The process of creating and pushing to the heap costs memory and processing time.

The minimum memory cost varies between JS engines, but 1K is a reasonable estimate for an empty context.

Note The exception is Proper tail call Functions that by nature of the return, may not require their own context, they use the calling functions context. (No browser current lets you use this ES6 required standard feature)

Recursive state stack

In general recursion is used as a way create a state stack. Each iteration (recursive) call creates a new function context, with closure that is pushed to the heap.

When the a function exits its state is deleted popped from the heap and the calling function's state is reinstated from the heap.

Example showing states pushed and popped

The following illustrates the state stack. The state includes a random value. Each iteration waits 200ms before creating the next. When the recursion exits each state is popped from the heap, the functions complete execution until the stack is clear.

    function resolveAfter200ms(x) {
      return new Promise(resolve => {
        setTimeout(() => resolve() , 200);
    async function test(count){
        const rand = Math.random() * 100 | 0;
        log(` push ${rand}`);
        if(count < 10){
            await resolveAfter200ms(count);
            await test(count+1);
        } else {
           log("<br>Top of stack<br>Popping function context states from heap<br>");
        log(` pop ${rand}`);
        if(count === 0){
           log("<br>Iteration complete");
    function log(data){
        document.body.innerHTML += `<span>${data}</span>`;

Why this makes your function bad.

Saving state to a stack is great when you have complex or branching data structures that you need to iterate.

Saving an irreverent state on the other hand is not so great. In fact I would say using an aysnc function to step over animation frames via recursion is about the worst way to create an animation.

If you have 200 characters to animate by the time they have all completed (the last recursive call) you have 200 function states on the heap.

I would estimate that the memory usage of a 200 character animation to be about 200K +, though that is nothing, I have seen people chew 1Meg just to add two integers in JavaScript (crazy eh!).

Luckily JavaScript makes such wasteful resource usage transparent, by providing ample memory and a gaggle of resource management threads to contain and clean up so from your point of view everything is running slick..

Another way.

I am a little long in the tooth and come from a time where 1K was all we had. I can not write code without a device angel whispering thoughts of "conserve, speed and memory"

So I would have written your code as follows if the requirement was to use async functions, and assuming that the waitAfter value is many times greater than 60fps.

// Note dont need text position (n)
async function typeWriter(text, elementId, waitAfter) {
    var n = 0;
    // Following DOM query only done once saving lots of time
    const el = document.getElementById(elementId);

    const wait = () => new Promise(r => setTimeout(r, waitAfter));

    // Preventing re flow overhead by using textContent rather than innerHTML
    const render = () => el.textContent = (text.substring(0, n + 1));

    while (n < text.length) {
        requestAnimationFrame(render);  // Calls existing function
                                        // thus avoid unneeded function state capture
                                        // via closure

        await wait();

        n++;                            // add after await so render gets
                                        // the correct value

From an external view its behaviour is identical, from a resource point of view its is remarkably different, will much lower memory use and associated GC overhead upon exit. It can also handle any size text, unlike the recursive method was limited to the available call stack size.

|improve this answer|||||
  • \$\begingroup\$ That was super helpful to me, thanks too much... unfortunately we as well as many -JavaScript developers- have too many resources to waste... 1KB should have landed a rocket the moon while now 1GB couldn't literally launch an Angular app smoothly! \$\endgroup\$ – Microsmsm Jan 17 '18 at 19:35

Not the answer you're looking for? Browse other questions tagged or ask your own question.