Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 28 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save elundmark/38d3596a883521cb24f5 to your computer and use it in GitHub Desktop.
Save elundmark/38d3596a883521cb24f5 to your computer and use it in GitHub Desktop.
Controlling the Frame Rate with requestAnimationFrame (http://codetheory.in/controlling-the-frame-rate-with-requestanimationframe/)

Saved for my personal reference, all content belongs to codetheory.in

Controlling the Frame Rate with requestAnimationFrame

Limiting the frame rate while using requestAnimationFrame can be a common want especially when coding Games where you want your animations and mechanics to not exceed a particular mark of frames per second. Let’s go through 2 ways of doing it.

Quick and Easy Way

Using setTimeout inside the rAF method is an easy way.

var fps = 30;
 
function draw() {
    setTimeout(function() {
        requestAnimationFrame(draw);
 
        // ... Code for Drawing the Frame ...
 
    }, 1000 / fps);
}
 
draw();

Nothing too fancy, simple enough!

Better Refined Approach

Ok, browsers cannot optimize setTimeout or setInterval. So it’s kinda better to do our own calculations and restrict the frame rate. Let’s see how.

var fps = 30;
var now;
var then = Date.now();
var interval = 1000/fps;
var delta;
  
function draw() {
     
    requestAnimationFrame(draw);
     
    now = Date.now();
    delta = now - then;
     
    if (delta > interval) {
        // update time stuffs
         
        // Just `then = now` is not enough.
        // Lets say we set fps at 10 which means
        // each frame must take 100ms
        // Now frame executes in 16ms (60fps) so
        // the loop iterates 7 times (16*7 = 112ms) until
        // delta > interval === true
        // Eventually this lowers down the FPS as
        // 112*10 = 1120ms (NOT 1000ms).
        // So we have to get rid of that extra 12ms
        // by subtracting delta (112) % interval (100).
        // Hope that makes sense.
         
        then = now - (delta % interval);
         
        // ... Code for Drawing the Frame ...
    }
}
 
draw();

Very simple code. All we do is set our frames per second and intervals between each frame. In the drawing function we deduct our last frame’s execution time from the current time to check whether the time elapsed since the last frame is more than our interval (which is based on the fps) or not. If the condition evaluates to true, we set the time for our current frame which is going to be the “last frame execution time” in the next drawing call.

Eventually all our drawing code is executed.

Demo Time

http://cssdeck.com/labs/gvxnxdrh/

@davidmcasas
Copy link

davidmcasas commented Jun 1, 2021

I tried the second approach, it worked perfectly fine on local, but when I uploaded the game to my server, every often the fps drop noticeably. Weird stuff. If I remove this thing, then the game plays normally on server. Any idea why that happens? My game is pure JS + HTML canvas with a few audio and image files, the whole thing is less than 2MB. Thank you for this anyway! Edit: I want to cap the framerate in case someone plays the game with a screen with a refresh rate higher than 60Hz

@ajmeese7
Copy link

@elundmark any chance you've got a newer link? I know it's been 7 years but just wanted to let you know that your link is dead!

@elundmark
Copy link
Author

elundmark commented May 24, 2022

@ajmeese7 Sorry no, but you could look into this program I wrote a while ago making good use of that concept. It works in any browser, it can even be run as a local file so it doesn't need to be hosted somewhere to work: file:///home/user/animated-pixel.html
Just download the file and open it in your browser.

All the code is contained to one single file, so just have a look at the code. The code you're looking for is on line 716

https://gist.github.com/elundmark/158a652ab1dedf98efc87c5f33ce6b91

@longnt80
Copy link

longnt80 commented Dec 17, 2022

@elundmark looking at this example, I think we can simplify the second approach by leveraging timestamp passed in from requestAnimationFrame and not using Date.now():

var fps = 30;
var interval = 1000/fps;
var then;
  
function draw(timestamp) {
     
    requestAnimationFrame(draw);

    // assign to 'then' for the first run
    if (then === undefined) {
        then = timestamp;
    }
     
    const delta = timestamp - then;
     
    if (delta > interval) {
        // update time stuffs
         
        // Just `then = now` is not enough.
        // Lets say we set fps at 10 which means
        // each frame must take 100ms
        // Now frame executes in 16ms (60fps) so
        // the loop iterates 7 times (16*7 = 112ms) until
        // delta > interval === true
        // Eventually this lowers down the FPS as
        // 112*10 = 1120ms (NOT 1000ms).
        // So we have to get rid of that extra 12ms
        // by subtracting delta (112) % interval (100).
        // Hope that makes sense.
         
        then = timestamp - (delta % interval);
         
        // ... Code for Drawing the Frame ...
    }
}
 
draw();

@chinoto
Copy link

chinoto commented Mar 7, 2023

The timestamp from rAF is based on the same epoch as performance.now(), so you can simplify the code by using var then=performance.now(); and removing the if-then.
This is what I ended up doing. The Math.min is to avoid building up a huge delta when the page is not active.

(async function renderLoop() {
    let then = performance.now();
    const interval = 1000 / 30;
    let delta = 0;
    while (true) {
        let now = await new Promise(requestAnimationFrame);
        if (now - then < interval - delta) {
            continue;
        }
        delta = Math.min(interval, delta + now - then - interval);
        then = now;

        // render code
    }
})();

@canadaduane
Copy link

Using @chinoto's code, here is an async generator version that I find quite tidy since it allows to separate the concern of "how or what" to render from "when" to render:

async function* nextFrame(fps) {
  let then = performance.now();
  const interval = 1000 / fps;
  let delta = 0;

  while (true) {
    let now = await new Promise(requestAnimationFrame);
    if (now - then < interval - delta) continue;
    delta = Math.min(interval, delta + now - then - interval);
    then = now;

    yield now;
  }
}

// I use it like this:
for await (const time of nextFrame(30 /* fps */)) {
   // ... render code
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment