使用requestAnimationFrame控制帧率?

221

现在看起来,requestAnimationFrame似乎是现在动画的事实标准。对我来说,在大部分情况下它运行得相当不错,但是现在我正在尝试做一些画布动画,并且我想知道:有没有办法确保它以特定的帧率运行?我理解rAF的目的是为了始终平滑地进行动画,而且我可能会冒着使我的动画变得不连贯的风险,但是现在它似乎以相当任意的速度运行,我想知道是否有办法解决这个问题。

我可以使用setInterval,但我想要rAF提供的优化(尤其是在选项卡处于焦点时自动停止)。

如果有人想查看我的代码,基本上是这样的:

animateFlash: function() {
    ctx_fg.clearRect(0,0,canvasWidth,canvasHeight);
    ctx_fg.fillStyle = 'rgba(177,39,116,1)';
    ctx_fg.strokeStyle = 'none';
    ctx_fg.beginPath();
    for(var i in nodes) {
        nodes[i].drawFlash();
    }
    ctx_fg.fill();
    ctx_fg.closePath();
    var instance = this;
    var rafID = requestAnimationFrame(function(){
        instance.animateFlash();
    })

    var unfinishedNodes = nodes.filter(function(elem){
        return elem.timer < timerMax;
    });

    if(unfinishedNodes.length === 0) {
        console.log("done");
        cancelAnimationFrame(rafID);
        instance.animate();
    }
}

其中Node.drawFlash()只是一些代码,根据计数变量确定半径,然后绘制一个圆。


3
你的动画是否有卡顿?我认为requestAnimationFrame最大的优势是只在需要时请求一个动画帧(正如其名称所暗示的那样)。比如说,如果你展示了一个静态的黑色画布,你应该得到0帧每秒,因为不需要新的帧。但是,如果你正在展示一个需要60fps的动画,你也应该得到那么多帧数。rAF允许“跳过”无用的帧,从而节省CPU。 - maxdec
setInterval 在非活动选项卡中不起作用。 - ViliusL
2
这段代码在90hz显示器、60hz显示器和144hz显示器上运行方式不同。 - manthrax
15个回答

264

如何将 requestAnimationFrame 控制在特定帧率

演示控制在 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

该方法的原理是测试自上一帧循环执行以来的经过时间。

只有当指定的 FPS 时间间隔已经过去时,您的绘画代码才会执行。

代码的第一部分设置了一些用于计算经过时间的变量。

var stop = false;
var frameCount = 0;
var $results = $("#results");
var fps, fpsInterval, startTime, now, then, elapsed;


// initialize the timer variables and start the animation

function startAnimating(fps) {
    fpsInterval = 1000 / fps;
    then = Date.now();
    startTime = then;
    animate();
}

而这段代码是实际的requestAnimationFrame循环,它以您指定的FPS进行绘制。

// the animation loop calculates time elapsed since the last loop
// and only draws if your specified fps interval is achieved

function animate() {

    // request another frame

    requestAnimationFrame(animate);

    // calc elapsed time since last loop

    now = Date.now();
    elapsed = now - then;

    // if enough time has elapsed, draw the next frame

    if (elapsed > fpsInterval) {

        // Get ready for next frame by setting then=now, but also adjust for your
        // specified fpsInterval not being a multiple of RAF's interval (16.7ms)
        then = now - (elapsed % fpsInterval);

        // Put your drawing code here

    }
}

29
不错的演示 - 应该被接受。我在这里fork了你的fiddle,用window.performance.now()代替了Date.now()进行演示。这很好地配合了rAF已经接收到的高分辨率时间戳,因此没有必要在回调函数内调用Date.now():http://jsfiddle.net/chicagogrooves/nRpVD/2/ - Dean Radcliffe
2
感谢您使用新的rAF时间戳功能更新链接。新的rAF时间戳添加了有用的基础架构,而且它比Date.now更精确。 - markE
16
这是一个非常棒的演示,它激发了我制作自己的演示(JSFiddle)。主要的区别在于使用了rAF(像Dean的演示)而不是Date,添加了控件来动态调整目标帧率,在动画和采样帧率之间分别设置了不同的时间间隔,并添加了历史帧率图表。 - tavnab
10
你所能控制的只有什么时候跳过一帧。60帧每秒的显示器总是在16毫秒间隔内绘制。例如,如果你希望游戏以50帧每秒运行,则需要跳过每6帧。你检查是否已经过去了20毫秒(1000/50),但实际上只过去了16毫秒,因此你跳过一帧,接着下一帧已经过去了32毫秒,因为你进行了绘制并重置。但是这样会跳过一半的帧并以30帧每秒的速度运行。所以当你进行重置时,你要记住上次等待了12毫秒太久。因此,下一帧又过去了16毫秒,但你将其计为16+12=28毫秒,然后进行绘制,这样你就等待了8毫秒太久。 - Curtis
1
不过需要注意的是,它不会显示当前 FPS,而是显示自启动以来的平均 FPS。如果您设置了更高的 FPS 限制,随着时间的推移,它将开始变慢。 - vanowm
显示剩余14条评论

76

我建议将你对requestAnimationFrame的调用包裹在一个setTimeout中:

const fps = 25;
function animate() {
  // perform some animation task here

  setTimeout(() => {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}
animate();
你需要在setTimeout内调用requestAnimationFrame,而不是反过来,因为requestAnimationFrame会安排你的函数在下一次重绘之前运行,如果你使用setTimeout进一步延迟更新,则会错过该时间窗口。但是,反过来做是可行的,因为你只需要在发出请求之前等待一段时间。

3
这似乎真的可以降低帧率,从而不会使我的CPU过热。而且它非常简单。干杯! - phocks
这是一个不错的、简单的方法,适用于轻量级动画。但是在某些设备上,它会有点不同步。我曾在我的一个早期引擎上使用过这种技术。在事情变得复杂之前,它工作得很好。最大的问题是当连接到方向传感器时,它会落后或者跳动。后来,我发现使用独立的setInterval,并通过对象属性在传感器、setInterval帧和RAF帧之间进行通信更新,可以使传感器和RAF实时运行,而动画时间可以通过setInterval的属性更新来控制。 - jdmayfield
1
我的显示器是60 FPS,如果我设置变量fps=60,使用这个代码我只能得到大约50 FPS。我想将其减慢到60,因为有些人拥有120 FPS的显示器,但我不想影响其他人。这非常困难。 - Curtis
4
你得到比预期更低的FPS的原因是因为setTimeout可能会在超过指定延迟后执行回调。这有许多可能的原因。每次循环都需要花费时间来设置新的计时器并执行一些代码,然后再设置新的超时。你无法准确地处理这个问题,你应该始终考虑到比预期慢一些的结果,但只要你不知道它会慢多少,试图降低延迟也是不准确的。浏览器中的JS并不意味着要如此精确。 - pdepmcp
这会导致明显的卡顿,不应在任何生产游戏中使用。 - aggregate1166877
显示剩余2条评论

64

更新 2016/6

导致帧率受限的问题在于屏幕具有恒定的更新速率,通常为60 FPS。

如果我们想要24 FPS,我们将永远无法在屏幕上获得真正的24 fps,我们可以按照这样的时间计算,但不能显示它,因为监视器只能以15 fps、30 fps或60 fps(一些监视器也是120 fps)同步显示帧。

然而,为了计时目的,我们可以在可能的情况下计算和更新。

您可以通过封装计算和回调到对象中来构建控制帧率的所有逻辑:

function FpsCtrl(fps, callback) {

    var delay = 1000 / fps,                               // calc. time per frame
        time = null,                                      // start time
        frame = -1,                                       // frame count
        tref;                                             // rAF time reference

    function loop(timestamp) {
        if (time === null) time = timestamp;              // init start time
        var seg = Math.floor((timestamp - time) / delay); // calc frame no.
        if (seg > frame) {                                // moved to next frame?
            frame = seg;                                  // update
            callback({                                    // callback function
                time: timestamp,
                frame: frame
            })
        }
        tref = requestAnimationFrame(loop)
    }
}
然后添加一些控制器和配置代码:
// play status
this.isPlaying = false;

// set frame-rate
this.frameRate = function(newfps) {
    if (!arguments.length) return fps;
    fps = newfps;
    delay = 1000 / fps;
    frame = -1;
    time = null;
};

// enable starting/pausing of the object
this.start = function() {
    if (!this.isPlaying) {
        this.isPlaying = true;
        tref = requestAnimationFrame(loop);
    }
};

this.pause = function() {
    if (this.isPlaying) {
        cancelAnimationFrame(tref);
        this.isPlaying = false;
        time = null;
        frame = -1;
    }
};

使用方法

现在变得非常简单 - 我们只需要像这样创建一个实例,设置回调函数和所需的帧率:

var fc = new FpsCtrl(24, function(e) {
     // render each frame here
  });
然后开始(如果需要,这可能是默认行为):
fc.start();
这就是全部,所有的逻辑都在内部处理。

演示

var ctx = c.getContext("2d"), pTime = 0, mTime = 0, x = 0;
ctx.font = "20px sans-serif";

// update canvas with some information and animation
var fps = new FpsCtrl(12, function(e) {
 ctx.clearRect(0, 0, c.width, c.height);
 ctx.fillText("FPS: " + fps.frameRate() + 
                 " Frame: " + e.frame + 
                 " Time: " + (e.time - pTime).toFixed(1), 4, 30);
 pTime = e.time;
 var x = (pTime - mTime) * 0.1;
 if (x > c.width) mTime = pTime;
 ctx.fillRect(x, 50, 10, 10)
})

// start the loop
fps.start();

// UI
bState.onclick = function() {
 fps.isPlaying ? fps.pause() : fps.start();
};

sFPS.onchange = function() {
 fps.frameRate(+this.value)
};

function FpsCtrl(fps, callback) {

 var delay = 1000 / fps,
  time = null,
  frame = -1,
  tref;

 function loop(timestamp) {
  if (time === null) time = timestamp;
  var seg = Math.floor((timestamp - time) / delay);
  if (seg > frame) {
   frame = seg;
   callback({
    time: timestamp,
    frame: frame
   })
  }
  tref = requestAnimationFrame(loop)
 }

 this.isPlaying = false;
 
 this.frameRate = function(newfps) {
  if (!arguments.length) return fps;
  fps = newfps;
  delay = 1000 / fps;
  frame = -1;
  time = null;
 };
 
 this.start = function() {
  if (!this.isPlaying) {
   this.isPlaying = true;
   tref = requestAnimationFrame(loop);
  }
 };
 
 this.pause = function() {
  if (this.isPlaying) {
   cancelAnimationFrame(tref);
   this.isPlaying = false;
   time = null;
   frame = -1;
  }
 };
}
body {font:16px sans-serif}
<label>Framerate: <select id=sFPS>
 <option>12</option>
 <option>15</option>
 <option>24</option>
 <option>25</option>
 <option>29.97</option>
 <option>30</option>
 <option>60</option>
</select></label><br>
<canvas id=c height=60></canvas><br>
<button id=bState>Start/Stop</button>

新答案

requestAnimationFrame 的主要目的是将更新与显示器的刷新率同步。这将需要您以显示器的 FPS 或其倍数进行动画处理(例如,对于典型刷新率为 60 Hz 的显示器,使用 60、30、15 FPS)。

如果您想要更任意的 FPS,则没有使用 rAF 的必要,因为帧速率永远不会与显示器的更新频率匹配(只有偶尔出现一两个帧),这根本无法提供平滑的动画效果(与所有帧重新计时一样),您可以考虑使用 setTimeoutsetInterval

这也是专业视频行业中已知的问题,当您想要以不同的 FPS 播放视频时,设备显示它的刷新速率可能会不同。许多技术已经被使用,例如帧混合和基于运动向量的复杂重新计时重建中间帧,但是在 Canvas 中这些技术不可用,结果将始终是生硬的视频。

var FPS = 24;  /// "silver screen"
var isPlaying = true;

function loop() {
    if (isPlaying) setTimeout(loop, 1000 / FPS);

    ... code for frame here
}
我们之所以将setTimeout放在前面(当使用poly-fill时,有些人会将rAF放在前面),是因为这样更加准确。当循环开始时,setTimeout会立即排队一个事件,因此无论剩余的代码使用了多少时间(只要不超过超时间隔),下一次调用都将在它所代表的间隔处进行(对于纯rAF而言,这并非必要,因为rAF无论如何都会尝试跳到下一帧)。 还值得注意的是,将其放在前面也会存在调用堆积的风险,就像setInterval一样。对于这种情况,setInterval可能会稍微更加准确。 你可以在循环之外使用setInterval来完成相同的任务。
var FPS = 29.97;   /// NTSC
var rememberMe = setInterval(loop, 1000 / FPS);

function loop() {

    ... code for frame here
}

停止循环的方法:

clearInterval(rememberMe);
为了在标签页失焦时降低帧率,您可以添加以下因素:

var isFocus = 1;
var FPS = 25;

function loop() {
    setTimeout(loop, 1000 / (isFocus * FPS)); /// note the change here

    ... code for frame here
}

window.onblur = function() {
    isFocus = 0.5; /// reduce FPS to half   
}

window.onfocus = function() {
    isFocus = 1; /// full FPS
}

通过这种方式,您可以将FPS降低到1/4等。


4
有时候你不是在尝试匹配显示器的帧率,而是在像图像序列这样的情况下删除帧。顺便说一句,解释得非常好。 - sidonaldson
3
使用requestAnimationFrame进行节流的最大原因之一是将某些代码的执行与浏览器的动画帧进行对齐。这样做可以使得运行更加平滑,特别是如果你需要每一帧都处理数据逻辑,比如音乐可视化等场景。 - Chris Dolphin
4
这是不好的,因为requestAnimationFrame的主要用途是同步 DOM 操作(读写),不使用它将会影响性能,因为操作不会有序地排队在一起执行,这将不必要地强制进行布局重绘。 - vsync
1
由于JavaScript是单线程运行的,而且在代码运行时不会触发超时事件,因此不存在“调用堆积”的风险。因此,如果函数执行时间超过超时时间,它将尽可能快地运行,而浏览器仍然会在调用之间进行重绘并触发其他超时。 - dronus
我知道你说页面刷新速度不能超过显示器的fps限制。但是,通过触发页面回流是否可能实现更快的刷新?反之,如果多个页面回流的速度快于本机fps速率,是否可能不会注意到它们? - Travis J

32

这些理论上都是好主意,但实际操作时会遇到问题。问题在于你无法对 requestAnimationFrame(RAF)进行节流控制,否则它就会失去作用。 因此,您需要让它以全速运行,并在单独的循环中更新数据,甚至可以使用单独的线程来做这个事情!

没错,您可以在浏览器中使用多线程 JavaScript!

我知道两种方法非常适用而且不会出现卡顿,它们消耗更少的能源并产生更少的热量。精准的人类时间尺度和机器效率是最终结果。

如果这有点啰嗦,那么接下来请看...


方法 1:使用 setInterval 更新数据,使用 RAF 更新图形。

使用一个单独的 setInterval 来更新每个动画元素的平移和旋转值、物理、碰撞等数据。将这些值保存在对象中,为每个对象分配变换字符串。将这些对象存储在数组中。将间隔设置为所需的 fps 的毫秒数:ms =(1000 / fps)。这可以保持稳定的时钟,使任何设备上的 fps 相同,而不受 RAF 速度的影响。不要在这里将变换分配给元素!

在 requestAnimationFrame 循环中,使用老式的 for 循环迭代您的数组,不要在这里使用较新的形式,因为它们速度较慢!

for(var i=0; i<sprite.length-1; i++){  rafUpdate(sprite[i]);  }

在你的rafUpdate函数中,从数组中的js对象中获取变换字符串及其元素id。你应该已经将'sprite'元素附加到一个变量中或通过其他方式轻松访问,以便在RAF中不需要花费时间进行“get”。将它们保留在一个以其html id命名的对象中,这样做非常有效。在进入SI或RAF之前设置好。

只使用3D变换(即使是2D),并使用RAF仅更新您的变换,对预计会发生更改的元素设置css“will-change: transform;”。这可以尽可能地使您的变换与本机刷新率同步,激活GPU,并告诉浏览器重点关注哪些部分。

因此,您应该拥有类似于以下伪代码...

// refs to elements to be transformed, kept in an array
var element = [
   mario: document.getElementById('mario'),
   luigi: document.getElementById('luigi')
   //...etc.
]

var sprite = [  // read/write this with SI.  read-only from RAF
   mario: { id: mario  ....physics data, id, and updated transform string (from SI) here  },
   luigi: {  id: luigi  .....same  }
   //...and so forth
] // also kept in an array (for efficient iteration)

//update one sprite js object
//data manipulation, CPU tasks for each sprite object
//(physics, collisions, and transform-string updates here.)
//pass the object (by reference).
var SIupdate = function(object){
  // get pos/rot and update with movement
  object.pos.x += object.mov.pos.x;  // example, motion along x axis
  // and so on for y and z movement
  // and xyz rotational motion, scripted scaling etc

  // build transform string ie
  object.transform =
   'translate3d('+
     object.pos.x+','+
     object.pos.y+','+
     object.pos.z+
   ') '+

   // assign rotations, order depends on purpose and set-up. 
   'rotationZ('+object.rot.z+') '+
   'rotationY('+object.rot.y+') '+
   'rotationX('+object.rot.x+') '+

   'scale3d('.... if desired
  ;  //...etc.  include 
}


var fps = 30; //desired controlled frame-rate


// CPU TASKS - SI psuedo-frame data manipulation
setInterval(function(){
  // update each objects data
  for(var i=0; i<sprite.length-1; i++){  SIupdate(sprite[i]);  }
},1000/fps); //  note ms = 1000/fps


// GPU TASKS - RAF callback, real frame graphics updates only
var rAf = function(){
  // update each objects graphics
  for(var i=0; i<sprite.length-1; i++){  rAF.update(sprite[i])  }
  window.requestAnimationFrame(rAF); // loop
}

// assign new transform to sprite's element, only if it's transform has changed.
rAF.update = function(object){     
  if(object.old_transform !== object.transform){
    element[object.id].style.transform = transform;
    object.old_transform = object.transform;
  }
} 

window.requestAnimationFrame(rAF); // begin RAF

这可以确保您对数据对象和变换字符串的更新与 SI 中所需的“帧”速率同步,而实际的变换分配与 GPU 刷新速率在 RAF 中同步。因此,实际的图形更新仅在 RAF 中进行,但是数据的更改以及构建变换字符串都在 SI 中进行,因此不会出现卡顿,而“时间”以所需的帧速率流动。


流程:

[setup js sprite objects and html element object references]

[setup RAF and SI single-object update functions]

[start SI at percieved/ideal frame-rate]
  [iterate through js objects, update data transform string for each]
  [loop back to SI]

[start RAF loop]
  [iterate through js objects, read object's transform string and assign it to it's html element]
  [loop back to RAF]
方法2:将SI放在Web Worker中。这种方法非常快且流畅!与方法1相同,但将SI放入Web Worker中。它将在完全不同的线程上运行,页面只需处理RAF和UI。来回传递精灵数组作为“可转移对象”。它非常快速,不需要克隆或序列化时间,但不像通过引用传递,另一边的引用被销毁,因此您需要让双方互相传递,并仅在存在时更新它们,就像在高中时互相传递笔记一样。只有一个人可以读写。只要检查它是否未定义以避免错误,这就很好了。RAF很快,会立即将其发送回来,然后通过大量GPU帧仅检查它是否已经发送回来。 Web Worker中的SI大多数时间都会有精灵数组,并更新位置,运动和物理数据,以及创建新的变换字符串,然后将其传递回页面中的RAF。这是我知道的通过脚本动画元素的最快方法。这两个函数将作为两个单独的程序在两个单独的线程上运行,以一种单个JS脚本无法做到的方式利用多核CPU。多线程JavaScript动画。这样做不会有任何卡顿,而是以实际指定的帧速率平稳运行,发散非常小。

1
顺便提一下,在方法1中,如果setInterval中的活动过多,可能会由于单线程异步而减慢RAF。您可以通过将该活动分解到多个SI帧上来缓解这种情况,这样异步就会更快地将控制权传递回RAF。请记住,RAF以最大帧速率运行,但会将图形更改与显示同步,因此跳过几个RAF帧是可以的,只要不跳过超过SI帧就不会出现卡顿。 - jdmayfield
2
方法2更加健壮,因为它实际上是在多任务处理两个循环,而不是通过异步来来回切换。但是,您仍然希望避免SI帧的持续时间超过所需的帧速率,因此如果SI活动涉及大量数据操作且需要超过一个SI帧才能完成,则分割SI活动仍然是可取的。 - jdmayfield
2
我认为这个解决方案存在问题,即当rAF被暂停时(例如用户切换到另一个标签页),它仍然在运行。 - N4ppeL
2
你不需要这样做。你可以在Web Worker中进行计算并传递结果消息。除此之外,你仍然以相同的方式运行RAF。你也可以通过iframe类似地运行另一个线程。消息传递基本上是相同的。我没有尝试过iframe的想法。无论哪种方式,它都会将计算放在与运行RAF和间隔帧的部分不同的线程中。 - jdmayfield
1
为什么你在 "length-1" 上使用 "小于"?这样你永远不会更新最后一个元素。for(var i=0; i<sprite.length-1; i++) - R. Navega
显示剩余14条评论

14
如何轻松地将帧率限制到特定的FPS:
// timestamps are ms passed since document creation.
// lastTimestamp can be initialized to 0, if main loop is executed immediately
var lastTimestamp = 0,
    maxFPS = 30,
    timestep = 1000 / maxFPS; // ms for each frame

function main(timestamp) {
    window.requestAnimationFrame(main);

    // skip if timestep ms hasn't passed since last frame
    if (timestamp - lastTimestamp < timestep) return;

    lastTimestamp = timestamp;

    // draw frame here
}

window.requestAnimationFrame(main);

来源:Isaac Sukin的详细解释:JavaScript游戏循环和时间


5
如果我的显示器以60FPS运行,而我希望我的游戏以58FPS运行,我会将maxFPS设置为58,这将使它以30FPS运行,因为它将跳过每第二帧。 - Curtis
2
是的,我也尝试过这个方法。我选择不对RAF本身进行节流,只有通过setTimeout更新更改。至少在Chrome中,这会导致有效的fps按照setTimeout的速度运行,根据DevTools中的读数。当然,它只能以视频卡和监视器刷新率的速度更新真实的视频帧,但这种方法似乎具有最少的抖动,因此控制“表面”fps最平稳,这正是我想要的。 - jdmayfield
2
由于我将JS对象中的所有运动与RAF分开跟踪,因此这使得动画逻辑、碰撞检测或任何你需要的东西以感知一致的速率运行,无论是RAF还是setTimeout,只需进行一些额外的数学计算。 - jdmayfield
1
在回答中提到很重要的一点是,requestAnimationFrame 回调函数自动获得一个参数,该参数类似于 performance.now() 返回的值,指示 requestAnimationFrame() 开始执行回调函数的时间点。 MDN - vsync

8

最简单的方法

注意:在不同刷新率的屏幕上可能会呈现不同的行为。


const FPS = 30;
let lastTimestamp = 0;


function update(timestamp) {
  requestAnimationFrame(update);
  if (timestamp - lastTimestamp < 1000 / FPS) return;
  
  
   /* <<< PUT YOUR CODE HERE >>>  */

 
  lastTimestamp = timestamp;
}


update();


它是否稳定? - user15307601
我将FPS设置为65(而不是60),以在120HZ移动屏幕和60HZ桌面上实现稳定的60 fps。 - Wo997
这就是我的方式,但是当帧生成时间超过周期时,它们开始堆积; 我得到了递归 rAF 调用,浏览器会挂起和冻结。我会在这个页面上使用其他方法。 - OsamaBinLogin

6
解决这个问题的简单方法是在不需要渲染帧的情况下从渲染循环中返回:
const FPS = 60;
let prevTick = 0;    

function render() 
{
    requestAnimationFrame(render);

    // clamp to fixed framerate
    let now = Math.round(FPS * Date.now() / 1000);
    if (now == prevTick) return;
    prevTick = now;

    // otherwise, do your stuff ...
}

重要的是要知道requestAnimationFrame取决于用户的显示器刷新率(vsync)。所以,如果您在模拟中没有使用单独的计时器机制,仅依赖requestAnimationFrame来控制游戏速度的话,在200Hz显示器上将无法进行。


这是我在three.js v106上尝试过唯一有效的解决方案。 - spacorum

5
var time = 0;
var time_framerate = 1000; //in milliseconds

function animate(timestamp) {
  if(timestamp > time + time_framerate) {
    time = timestamp;    

    //your code
  }

  window.requestAnimationFrame(animate);
}

2
请添加一些句子来解释您的代码在做什么,这样您就可以获得更多的赞同票。 - Fuzzy Analysis

4
简单解释之前的回答。如果你想要实时、准确的限流而不会出现卡顿或丢帧,那么需要考虑GPU和CPU的友好性。 setInterval和setTimeout都是面向CPU的,而不是面向GPU的。 requestAnimationFrame则是纯粹面向GPU的。 将它们分开运行即可。在setInterval中更新数学计算并创建一个小的CSS脚本字符串。在RAF循环中,只使用该脚本来更新元素的新坐标。在RAF循环中不要做其他事情。 RAF与GPU固有地绑定在一起。每当脚本不改变时(例如因为SI运行得比较慢),基于Chromium的浏览器就知道它们不需要做任何事情,因为没有变化。这意味着在60次每秒的60个RAF GPU帧中,即使实时创建了“frame”,但它知道没有任何变化,最终结果是它不会浪费任何能量。如果你在DevTools中检查,你会看到你的GPU帧率以setInterval所规定的速率注册。 真的很简单。将它们分开运行,它们就会相互配合。 没有卡顿。

2
我总是用这种非常简单的方式完成它,而不需要处理时间戳:
let fps, eachNthFrame, frameCount;

fps = 30;

//This variable specifies how many frames should be skipped.
//If it is 1 then no frames are skipped. If it is 2, one frame 
//is skipped so "eachSecondFrame" is renderd.
eachNthFrame = Math.round((1000 / fps) / 16.66);

//This variable is the number of the current frame. It is set to eachNthFrame so that the 
//first frame will be renderd.
frameCount = eachNthFrame;

requestAnimationFrame(frame);

//I think the rest is self-explanatory
function frame() {
  if (frameCount === eachNthFrame) {
    frameCount = 0;
    animate();
  }
  frameCount++;
  requestAnimationFrame(frame);
}

4
如果你的显示器刷新率是120帧每秒,这个会运行得太快了。 - Curtis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,