Skip to content

Instantly share code, notes, and snippets.

@addyosmani
Last active September 23, 2023 03:37
Show Gist options
  • Star 60 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save addyosmani/5434533 to your computer and use it in GitHub Desktop.
Save addyosmani/5434533 to your computer and use it in GitHub Desktop.
Limit the frame-rate being targeted with requestAnimationFrame
/*
limitLoop.js - limit the frame-rate when using requestAnimation frame
Released under an MIT license.
When to use it?
----------------
A consistent frame-rate can be better than a janky experience only
occasionally hitting 60fps. Use this trick to target a specific frame-
rate (e.g 30fps, 48fps) until browsers better tackle this problem
natively.
Please ensure that if you're using this workaround, you've done your best
to find and optimize the performance bottlenecks in your application first.
60fps should be an attainable goal. If however you've tried your best and
are still not getting the desired frame-rate, see if you can get some mileage
with it.
This type of trick works better when you know you have a fixed amount
of work to be done and it will always take longer than 16.6ms. It doesn't
work as well when your workload is somewhat variable.
Solution
----------------
When we draw, deduct the last frame's execution time from the current
time to see if the time elapsed since the last frame is more than the
fps-based interval or not. Should the condition evaluate to true, set
the time for the current frame which will be the last frame execution
time in the next drawing call.
Prior art / inspiration
------------------------
http://cssdeck.com/labs/embed/gvxnxdrh/0/output
http://codetheory.in/controlling-the-frame-rate-with-requestanimationframe/
*/
var limitLoop = function (fn, fps) {
// Use var then = Date.now(); if you
// don't care about targetting < IE9
var then = new Date().getTime();
// custom fps, otherwise fallback to 60
fps = fps || 60;
var interval = 1000 / fps;
return (function loop(time){
requestAnimationFrame(loop);
// again, Date.now() if it's available
var now = new Date().getTime();
var delta = now - then;
if (delta > interval) {
// Update time
// now - (delta % interval) is an improvement over just
// using then = now, which can end up lowering overall fps
then = now - (delta % interval);
// call the fn
fn();
}
}(0));
};
/*
Feel free to play with this over at http://jsfiddle.net/addyo/Y8P6S/1/.
You can either use the Chrome DevTools Timeline or FPS counter to confirm
if you're hitting a consistent fps.
*/
// rAF normalization
window.requestAnimationFrame = function() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
window.oRequestAnimationFrame ||
function(f) {
window.setTimeout(f,1e3/60);
}
}();
// define a reference to the canvas's 2D context
var context = television.getContext('2d');
// create a buffer to hold the pixel data
var pixelBuffer = context.createImageData(television.width, television.height);
function drawStatic() {
var color,
data = pixelBuffer.data,
index = 0,
len = data.length;
while (index < len) {
// choose a random grayscale color
color = Math.floor(Math.random() * 0xff);
// red, green and blue are set to the same color
// to result in a random gray pixel
data[index++] = data[index++] = data[index++] = color;
// the fourth multiple is always completely opaque
data[index++] = 0xff; // alpha
}
// flush our pixel buffer to the canvas
context.putImageData(pixelBuffer, 0, 0);
};
limitLoop(drawStatic, 30);
@thebuilder
Copy link

Why not just use the time variable passed from requestAnimationFrame, instead of var now = new Date().getTime();?:)

@jpblancoder
Copy link

Wouldn't this be better?

return (function loop(time){
        // again, Date.now() if it's available
        var now = new Date().getTime();
        var delta = now - then;
 
        if (delta > interval) {
            // Update time
            // now - (delta % interval) is an improvement over just 
            // using then = now, which can end up lowering overall fps
            then = now - (delta % interval);
 
            // call the fn
            requestAnimationFrame(fn);
        }
    }(0));

@jeongsd
Copy link

jeongsd commented Mar 5, 2017

how about this?

class AnimationFrame {
  constructor(animate, fps = 60) {
    this.requestID = 0;
    this.fps = fps;
    this.animate = animate;
  }

  start() {
    let then = performance.now();
    const interval = 1000 / this.fps;

    const animateLoop = (now) => {
      this.requestID = requestAnimationFrame(animateLoop);
      const delta = now - then;

      if (delta > interval) {
        then = now - (delta % interval);
        this.animate(delta);
      }
    };
    this.requestID = requestAnimationFrame(animateLoop);
  }

  stop() {
    cancelAnimationFrame(this.requestID);
  }

}

@Kouty
Copy link

Kouty commented Apr 12, 2017

@jeongsd

jeongsd how about this?
...

It seems a good solution, but I would add the following corrections:

  • tolerance = 0.1; // RAF could call the callback a little bit earlier
  • if (delta >= interval - tolerance)
class AnimationFrame {
  constructor(fps = 60, animate) {
    this.requestID = 0;
    this.fps = fps;
    this.animate = animate;
  }

  start() {
    let then = performance.now();
    const interval = 1000 / this.fps;
    const tolerance = 0.1;

    const animateLoop = (now) => {
      this.requestID = requestAnimationFrame(animateLoop);
      const delta = now - then;

      if (delta >= interval - tolerance) {
        then = now - (delta % interval);
        this.animate(delta);
      }
    };
    this.requestID = requestAnimationFrame(animateLoop);
  }

  stop() {
    cancelAnimationFrame(this.requestID);
  }

}

@jeongsd
Copy link

jeongsd commented Jun 16, 2017

sorry answer to late @Kouty that's nice

@demoon84
Copy link

demoon84 commented Sep 9, 2017

@Kouty Hi Kouty! what is the "tolerance(0.1)"?

@Kouty
Copy link

Kouty commented Nov 3, 2017

Sorry for the late answer.
tolerance(0.1) is a parameter to tolerate timing/rounding errors. For example, requestAnimationFrame should run the callback only after 1000/60 milliseconds have passed. But sometimes the browser will run the callback a little bit earlier. If that happens inside the tolerance range, the callback will be executed anyway.
In the example above, tolerance = 0.1 means that a tick that happens 0.1 milliseconds before the interval will be executed.
For example let's assume we need 20 fps and the first tick (callback execution) happens at time 49.95 ms. Without tolerance, the tick won't be executed. With a tolerance of 0.1 ms, the tick will be executed.

@xinkule
Copy link

xinkule commented Mar 18, 2018

Can I ask a quesiton? I just couldn't figure out the reason using "delta % interval" instead of "delta - interval", I think in this case they are the same, or did I miss some point? Hope someone could explain, thx.

@Kouty
Copy link

Kouty commented Mar 19, 2018

(delta - interval) is wrong when delta >= 2 * interval. More in general a - b = a % b when b <= a < 2*b (a>0, b>0).

Let's make an example to clarify:
FPS = 20, interval = 50ms. tolerance = 0;
Formulas:
const delta = now - then;
then = now - (delta % interval);
thenMinus = now - (delta - interval);
if (delta >= interval - tolerance)

Ticks: 0ms - 110ms - 140ms
1 now = 110: delta = 110 - 0 = 110; then = 110 - (110 % 50) = 110 - 10 = 100 ✓; thenMinus = 110 - (110 - 50) = 110 - 60 = 50
2a (Using then) now = 140: delta = 140 - 100 = 40 ms; if (40 >= 50 - 0) => false. Must not tick since it must tick at 150ms
2b (Using thenMinus) now = 140: delta = 140 - 50 = 90 ms; if (90 >= 50 - 0) => true. It ticks, but it shouldn't

"then" represents the nearest multiple of interval <= now

@bnwa
Copy link

bnwa commented Sep 14, 2022

@jeongsd

jeongsd how about this?
...

It seems a good solution, but I would add the following corrections:

  • tolerance = 0.1; // RAF could call the callback a little bit earlier
  • if (delta >= interval - tolerance)
class AnimationFrame {
  constructor(fps = 60, animate) {
    this.requestID = 0;
    this.fps = fps;
    this.animate = animate;
  }

  start() {
    let then = performance.now();
    const interval = 1000 / this.fps;
    const tolerance = 0.1;

    const animateLoop = (now) => {
      this.requestID = requestAnimationFrame(animateLoop);
      const delta = now - then;

      if (delta >= interval - tolerance) {
        then = now - (delta % interval);
        this.animate(delta);
      }
    };
    this.requestID = requestAnimationFrame(animateLoop);
  }

  stop() {
    cancelAnimationFrame(this.requestID);
  }

}

What happens when the delta is twice the interval or more, e.g. rAF throttling when user switches tabs then comes back, then switches tabs, then comes back, etc? This formulation seems to fire the tick function no matter the ratio of interval to delta, information I intuit is destroyed by the modulo operation, or am I missing something?

Opening this pen which uses the modulo operation, it declares itself at 30FPS and stabilizes at 30FPS so long as the page is at the fore. But which I switch tabs for a few seconds, then come back, the FPS slows; if I repeat this the FPS will continue to slow each iteration.

If for whatever reason you can't pause when user switches tabs and comes back after some time, a solution would seem to be to calculate the ratio of interval to the (fairly large) delta and call your tick function Math.floor(intervalToDeltaRatio) times, or, if you're keeping track of what frame you're on, increment your frame counter by such, then remove the overage via the formulation in this gist.

@Kouty
Copy link

Kouty commented Sep 15, 2022

@bnwa This is not an example of game loop aiming to execute "animate" callback an exact number of times. The purpose of the example is "Limit the frame-rate being targeted with requestAnimationFrame" as the title suggests.

"What happens when the delta is twice the interval or more...This formulation seems to fire the tick function no matter the ratio of interval to delta". Yes, it aims to call the animate function as soon as possible but not before a certain interval, defined by the fps parameter.

Your pen example never resets the counter, which is not correct to evaluate the script above.

Depending on what you want top achieve, there are differente implementations of a game loop. Most of the time you don't care when the "animate" callback is called, you just want it to be called as soon as possible but after a certain interval of time. This is what setTimeout, setInterval or requestAnimationFrame all do.

Each implementations has its pro and cons. The solution proposed by you, for example, is useful when you need the "animation" callback fired with a fixed interval and a fixed number of times. A pool game would need a similar solution. But there are some cons: if the "animation" callback is very slow, the loop may never reach the present time, causing animations to run slower than intended (like 0.5x) for example. Also it will consume more battery, which in the mobile world is not good. Usually you cap the frame-rate to reduce battery consumption. Otherwise why not going to the maximum fps?

In summary, my proposed implementation works as intended: calling "animate" callback as soon as possible, but not before a given interval of time.

@bnwa
Copy link

bnwa commented Sep 15, 2022

@Kouty I think I see my misunderstanding, here we're solely interested in setting a ceiling for frame rate rather than both a ceiling and a floor?

@Kouty
Copy link

Kouty commented Sep 18, 2022

It is not just a matter of a ceiling and a floor. Here we are interested in making requestAnimationFrame work very similar to setInterval.
Any other feature atop the simplest implementation is an opinionated implementation.

What I mean is that one can build her own implementation based on a basic requestAnimationFrame according to the need she has. Each non basic implementation has its pros and cons.

For example, if you are implementing a real time multiplayer game, you need the rendering to be synchronized with the latest data from the server, so a game-loop in this case should sacrifice fluidity in rendering (skipping frames) in order to show what is happening right now.
On the other hand, a simple graphic animation probably would prefer to be smooth (avoid skipping frames) rather than having each frame perfectly synchronized.

What you propose, if I got it right, is to call the "animation" callback n-times to avoid skipping frames. This is one of the possible game-loops implementation.
A typical game-loop will not only call as soon as possible the "animate" callback to avoid skipping frames, but it will provide 2 callbacks, one that should be very light, called ideally each frame of the rendering, another, less light weighted, called with a much lower frame-rate to update the physics, for example.

Just calling the "animate" callback for each skipped frame, is not enough. What happens if your computer is not fast enough to compute the "animate" callback at the desired frame-rate? A naive implementation (just compute the number of skipped frames and call "animate") will stuck the browser and render the whole web app unusable.

In a few words, the question of this Gist could have been: "How do I implement a loop based on requestAnimationFrame so that it works very similar to setInterval?"

@dcgithub
Copy link

love your work dude thank you

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