Search code examples
javascriptperformancetimersettimeoutprecision

How to wait for a specific amount of time precisely in javascript


I'm measuring time between keystroykes when user types some text, using performance.now() and saving it. Now, I want to replay what user has been typing, potentially giving him the ability to race with himself, typing the same text again.

However, if I use the following construction:

for (let ev of events) {
    await new Promise(resolve => setTimeout(resolve, ev.performanceDelay));

    replayKey();
}

I noticed that the text that is being replayed is being typed slower than it has been typed before. For example, I type the whole text in 40 seconds, but replay is running for 45 seconds, rendering any possibility of racing with myself obsolete.

My question is: how can I make it work? Is there a way? I want to delay an action for a specific amount of time, preferably with precision down to microseconds (output of performance.now()), obviously without hanging the main thread or preventing any other events from occuring.

I don't mind delving into a specific API of a specific browser, for example Chrome (as I'm running it mainly). Is there a way to make this work?

Update

The only way I can think of to solve the "race" solution for now is to somehow aggregate key press events into larger chunks and replay them by these chunks, e.g. at least 20-50 ms length. However, I also want this as an analytics tool, so that the user can see in hindsight the simulation of his typing session to be able to spot the places where he is slow or makes mistakes etc. So it would be nice to be able to replay his exact inputs. But I'm starting to think it won't be possible because user can press keys almost instantly or even at the same time, but you can't really create near zero delays (I guess?) on the browser API.


Solution

  • Use requestAnimationFrame and timestamp offsets, don't rely on precision of setTimeout or requestAnimationFrame:

    let start = performance.now();
    const keys = [...'abcdefgiklmopqrstuwxyz'];
    const events = Array.from({length:100}, () => ({key: keys[Math.random()*keys.length|0], timestamp: start += Math.random() * 100}));
    
    (async()=>{
      let idx = 0;
      let start = performance.now();
      let from = events[0].timestamp;
      while(idx < events.length){
          const now = performance.now();
          const ev = events[idx];
          if(now - start >= ev.timestamp - from){
            $key.textContent = `${idx}: ${ev.key}`;
            idx++;
            continue; // if several keys are between frames, just issue them together 🤷‍♂️
          }
          await new Promise(resolve => requestAnimationFrame(resolve));
      }
    })();
    <div id="$key" style="font-size:32px"></div>