Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

Show comment
Hide comment
@thebuilder

thebuilder Sep 8, 2015

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

thebuilder commented Sep 8, 2015

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

@jpblancoder

This comment has been minimized.

Show comment
Hide comment
@jpblancoder

jpblancoder Jan 10, 2017

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));

jpblancoder commented Jan 10, 2017

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

This comment has been minimized.

Show comment
Hide comment
@jeongsd

jeongsd 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);
  }

}

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

This comment has been minimized.

Show comment
Hide comment
@Kouty

Kouty 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);
  }

}

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

This comment has been minimized.

Show comment
Hide comment
@jeongsd

jeongsd Jun 16, 2017

sorry answer to late @Kouty that's nice

jeongsd commented Jun 16, 2017

sorry answer to late @Kouty that's nice

@demoon84

This comment has been minimized.

Show comment
Hide comment
@demoon84

demoon84 Sep 9, 2017

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

demoon84 commented Sep 9, 2017

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

@Kouty

This comment has been minimized.

Show comment
Hide comment
@Kouty

Kouty 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.

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

This comment has been minimized.

Show comment
Hide comment
@xinkule

xinkule 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.

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

This comment has been minimized.

Show comment
Hide comment
@Kouty

Kouty 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

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

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