Typewriter animation, implemented using recursive asynchronous function 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) =&gt; new Promise(r =&gt; setTimeout(r, ms));

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




Full code

https://github.com/Microsmsm/microsmsm.github.io/blob/master/js/typewriter.js

Result:

https://microsmsm.com/
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 =&gt; {
        setTimeout(() =&gt; resolve() , 200);
      });
    };
    async function test(count){
        const rand = Math.random() * 100 | 0;
        log(` push ${rand}`);
        if(count &lt; 10){
            
            await resolveAfter200ms(count);
            await test(count+1);
        } else {
           log("&lt;br&gt;Top of stack&lt;br&gt;Popping function context states from heap&lt;br&gt;");
        }
        log(` pop ${rand}`);
        
        if(count === 0){
           log("&lt;br&gt;Iteration complete");
        }
    }
    function log(data){
        document.body.innerHTML += `&lt;span&gt;${data}&lt;/span&gt;`;
    }
    test(0);



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 &quot;conserve, speed and memory&quot;
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 = () =&gt; new Promise(r =&gt; setTimeout(r, waitAfter));

    // Preventing re flow overhead by using textContent rather than innerHTML
    const render = () =&gt; el.textContent = (text.substring(0, n + 1));
    
    while (n &lt; 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.