Writing a Run Loop in JavaScript & React

Luke Millar
projector_hq
Published in
5 min readApr 9, 2021

Those that are familiar with game development or animated graphics will know the power and necessity of a run loop. It’s not very often that we need them in standard web development but it is a powerful concept and tool that is great to have in your back pocket.

A Standard Run Loop

A run loop is just an infinite loop that we use to update the state of our world and then render that updated world. The simplest version of a run loop would look like this:

Loops in Javascript

In web development, this “while (true)” would not work for a number of reasons. The main one being that JavaScript runtime is single threaded so we’d block any other code that needs to run. The browser would completely freeze up and eventually crash.

We would need to give the thread a break after each render so that other code can run as needed. A common way we might do this is with a setTimeout.

Here, we queue up each loop in a setTimeout. This means we won’t freeze up the browser and we can continue to update our graphics as often as possible.

16.67ms

A standard monitor updates at 60fps (frames per second). This means the HIGHEST number of frames you can draw in a second is 60. Even if your updates were faster than that, your monitor would not be able to draw them.

At 60fps we get 16.67ms to calculate and draw each individual frame. That number is important because that means that if we want our graphics to render at a full 60fps, we need to both update and render our world in under 16.67ms. It doesn’t sound like much time, but if we’re deliberate about the work that we do, queue up expensive calculations, and plan around it, we can achieve that number.

So what happens if we do that setTimeout based run loop? Since we’re using a setTimeout(0) to queue up each run, it will start the next loop as soon as it finishes the previous one. This means if our updateWorld and renderWorld are pretty quick, we can easily run our loop way more than 60 times a second.

This is actually bad and undesired for 2 reasons:

  1. We should be doing as little work as possible. Updating our world more than 60 times in a second is unnecessary. It won’t draw more than 60 times a second so we should avoid evaluating it more than 60 times a second.
  2. The run function is not synchronized at all with the browser or the monitor. It’s possible to miss drawing entire frames because we were scheduled late by 1ms even if we could have been ready to draw earlier.

We could try to pick a better timeout number for queuing up our next loop but we’ll never be exact with how long ago our last loop started, how long it took to run, and when the next loop will actually begin. Getting that number right is impossible and unpredictable because we don’t know what other code is going to run.

requestAnimationFrame

Fortunately for us, the browser actually gives us an API to synchronize our loop to the actual frames it’s going to draw. It’s called requestAnimationFrame and it works just like setTimeout but it’s timed to the refresh rate of your monitor. Updating our loop to use requestAnimationFrame would look like this:

In this example, my run() function will now only run at a maximum of 60 times per second. It’ll run exactly once for each frame that the browser is going to draw. No extra calculations, no extra CPU cycles, no missed frames at all.

And this works regardless of your monitor’s refresh rate. So if you have a monitor with 120fps, your requestAnimationFrame will call you back 120 times a second instead of 60. You don’t have to time this yourself at all.

Beautiful.

Frame Time

The other problem we run into here is that if we’re trying to read the current time on each frame, that time can be inconsistent. Even though our callback is called once per frame, the exact millisecond that our function runs can run at any point between those 2 frames. So it might run at frame + 1ms in Frame1 and at frame + 11ms in Frame2.

So if we just read the current time in each frame, we might see some significant time differences between those frames that could make our animations not quite as smooth frame to frame.

Fortunately for us, requestAnimationFrame has a solution for this. When our callback is called, requestAnimationFrame actually gives us the frame time as an argument to our callback function. This frame time is guaranteed to be 16.67ms from the last frame time (as long as you are maintaining 60fps).

Run Loop in React

Now that we have a run loop working in JavaScript, now we need to get it working in our React component. This part is pretty simple conceptually.

  1. We have a useEffect to run once when the Component mounts.
  2. When the component “mounts” we initiate our run loop.
  3. On each frame we update a state variable with the new frameTime so we can use it in our component.
  4. On unmount we cancel our run loop.

useFrameTime React hook

If you want to make that logic reusable you can easily turn it into a custom hook.

Stopwatch Example

I wrote up a quick example of this run loop as a stopwatch in React on Code Sandbox.

Summary

There are a lot of use cases for a run loop like this on the web and in React, whether you’re writing your own custom animations or rendering real-time graphics through Canvas, SVG, or WebGL.

We put requestAnimationFrames to good use in our main run loop of the Projector editor and player. It’s how we handle everything from transition animations to real-time editing. If you want to hear more about it or are interested in working on these sorts of problems reach out to us at careers@projector.com. We’re hiring web engineers as well as backend systems engineers.

--

--