乐闻世界logo
搜索文章和话题

Why is requestanimationframe better than setinterval or settimeout

7 个月前提问
3 个月前修改
浏览次数18

2个答案

1
2

requestAnimationFrame(简写为rAF)之所以在性能上优于setIntervalsetTimeout,主要有以下几个原因:

1. 浏览器优化

requestAnimationFrame是专门为动画设计的API,浏览器知道您通过这个函数请求的回调是用于绘制动画的。因此,浏览器可以对动画进行优化,包括减少在不可见标签页中的动画的帧率,或者是在动画不在屏幕上时停止动画,这样可以提高性能并减少能耗。

2. 屏幕刷新率同步

requestAnimationFrame回调执行的频率通常与浏览器的屏幕刷新率同步。大多数屏幕有60Hz的刷新率,意味着屏幕每秒刷新60次。rAF会尽可能匹配这个频率,每次屏幕刷新时更新一次动画,从而创建平滑的视觉效果。而setIntervalsetTimeout则没有这样的机制,可能会导致动画出现掉帧,或者动画更新与屏幕刷新不同步,造成不必要的计算和屏幕撕裂。

3. 减少页面重排和重绘

使用requestAnimationFrame进行动画处理,浏览器可以将动画的视觉更新和DOM更新安排在同一个浏览器的绘制周期内,这样可以减少页面重排(layout)和重绘(repaint)的次数,提高性能。

4. CPU节能

当使用setIntervalsetTimeout时,如果设定的时间间隔很短,即使元素不可见,它们也会继续运行,这将不必要地占用CPU资源。requestAnimationFrame会智能调整,当用户切换到其他标签或最小化窗口时,动画会暂停,这有助于减少CPU消耗,特别是在移动设备上。

示例

考虑一个简单的动画例子,比如一个元素的左右滑动。使用setInterval可能会这样编写代码:

javascript
let position = 0; setInterval(function() { position += 5; element.style.transform = `translateX(${position}px)`; }, 16); // 大约对应60Hz刷新率的间隔

这段代码会尝试每16毫秒移动元素,但没有机制确保它与屏幕刷新同步。

而使用requestAnimationFrame,代码如下:

javascript
let position = 0; function step() { position += 5; element.style.transform = `translateX(${position}px)`; requestAnimationFrame(step); } requestAnimationFrame(step);

这里,动画逻辑与浏览器的刷新率同步,能够根据屏幕刷新情况智能调整执行频率。

综上所述,requestAnimationFrame提供了更高效、更平滑的动画体验,尤其是对于复杂或高性能需求的动画,这比使用setIntervalsetTimeout要好得多。

2024年6月29日 12:07 回复

高品质动画。

requestAnimationFrame``setTimeout产生更高质量的动画,完全消除使用或 时可能发生的闪烁和剪切setInterval,并减少或完全消除跳帧。

剪切

是在显示扫描中途将新的画布缓冲区呈现给显示缓冲区时,导致由不匹配的动画位置引起的剪切线。

闪烁

是在画布完全渲染之前将画布缓冲区呈现给显示缓冲区时引起的。

跳帧

这是由于渲染帧之间的时间与显示硬件不精确同步而引起的。每隔这么多帧就会跳过一帧,从而产生不一致的动画。(有一些方法可以减少这种情况,但我个人认为这些方法会产生更糟糕的总体结果)由于大多数设备每秒使用 60 帧(或倍数),导致每 16.666...ms 产生一个新帧,并且计时器并使用整setTimeout数值setInterval,它们永远无法完美匹配帧速率(如果有的话,四舍五入到 17ms interval = 1000/60


一个演示抵得上一千个字。

更新问题requestAnimationFrame循环不正确的fps的答案显示了setTimeout的帧时间如何不一致并将其与requestAnimationFrame进行比较。

该演示显示了一个简单的动画(条纹在屏幕上移动),单击鼠标按钮将在所使用的渲染更新方法之间切换。

使用了多种更新方法。动画工件的确切外观将取决于您正在运行的硬件设置。您会在条纹的运动中寻找轻微的抽搐

笔记。您可能关闭了显示同步或关闭了硬件加速,这将影响所有计时方法的质量。低端设备也可能在动画方面遇到问题

  • 计时器 使用 setTimeout 来设置动画。时间为1000/60

  • RAF 最佳质量,使用 requestAnimationFrame 进行动画处理

  • 双定时器,使用两个定时器,一个每 1000/60 清除调用一次,另一个用于渲染。

    2019 年 10 月更新计时器呈现内容的方式发生了一些变化。为了表明它setInterval无法与显示刷新正确同步,我更改了双计时器示例,以表明使用多个计时器setInterval仍然会导致严重闪烁。这将产生的闪烁程度取决于硬件设置。

  • RAF 具有定时动画,使用 requestAnimationFrame 但使用帧运行时间进行动画处理。这种技术在动画中很常见。我相信它有缺陷,但我把它留给观众

  • 带有定时动画的计时器。作为“具有定时动画的 RAF”,在本例中用于克服“计时器”方法中出现的跳帧。我再次认为这很糟糕,但游戏社区发誓这是当您无法访问显示刷新时使用的最佳方法

    /** SimpleFullCanvasMouse.js begin **/

    var backBuff; var bctx; const STRIPE_WIDTH = 250; var textWidth; const helpText = "Click mouse to change render update method."; var onResize = function(){ if(backBuff === undefined){ backBuff = document.createElement("canvas") ; bctx = backBuff.getContext("2d");

    shell
    } backBuff.width = canvas.width; backBuff.height = canvas.height; bctx.fillStyle = "White" bctx.fillRect(0,0,w,h); bctx.fillStyle = "Black"; for(var i = 0; i < w; i += STRIPE_WIDTH){ bctx.fillRect(i,0,STRIPE_WIDTH/2,h) ; } ctx.font = "20px arial"; ctx.textAlign = "center"; ctx.font = "20px arial"; textWidth = ctx.measureText(helpText).width;

    }; var tick = 0; var displayMethod = 0; var methods = "Timer,RAF Best Quality,Dual Timers,RAF with timed animation,Timer with timed animation".split(","); var dualTimersActive = false; var hdl1, hdl2

    function display(timeAdvance){ // put code in here

    shell
    tick += timeAdvance; tick %= w; ctx.drawImage(backBuff,tick-w,0); ctx.drawImage(backBuff,tick,0); if(textWidth !== undefined){ ctx.fillStyle = "rgba(255,255,255,0.7)"; ctx.fillRect(w /2 - textWidth/2, 0,textWidth,40); ctx.fillStyle = "black"; ctx.fillText(helpText,w/2, 14); ctx.fillText("Display method : " + methods[displayMethod],w/2, 34); } if(mouse.buttonRaw&1){ displayMethod += 1; displayMethod %= methods.length; mouse.buttonRaw = 0; lastTime = null; tick = 0; if(dualTimersActive) { dualTimersActive = false; clearInterval(hdl1); clearInterval(hdl2); updateMethods[displayMethod]() } }

    }

    //================================================================================================== // The following code is support code that provides me with a standard interface to various forums. // It provides a mouse interface, a full screen canvas, and some global often used variable // like canvas, ctx, mouse, w, h (width and height), globalTime // This code is not intended to be part of the answer unless specified and has been formated to reduce // display size. It should not be used as an example of how to write a canvas interface. // By Blindman67 const U = undefined;const RESIZE_DEBOUNCE_TIME = 100; var w,h,cw,ch,canvas,ctx,mouse,createCanvas,resizeCanvas,setGlobals,globalTime=0,resizeCount = 0; var L = typeof log === "function" ? log : function(d){ console.log(d); } createCanvas = function () { var c,cs; cs = (c = document.createElement("canvas")).style; cs.position = "absolute"; cs.top = cs.left = "0px"; cs.zIndex = 1000; document.body.appendChild(c); return c;} resizeCanvas = function () { if (canvas === U) { canvas = createCanvas(); } canvas.width = window.innerWidth; canvas.height = window.innerHeight; ctx = canvas.getContext("2d"); if (typeof setGlobals === "function") { setGlobals(); } if (typeof onResize === "function"){ resizeCount += 1; setTimeout(debounceResize,RESIZE_DEBOUNCE_TIME);} } function debounceResize(){ resizeCount -= 1; if(resizeCount <= 0){ onResize();}} setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; mouse.updateBounds(); } mouse = (function(){ function preventDefault(e) { e.preventDefault(); } var mouse = { x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0, over : false, bm : [1, 2, 4, 6, 5, 3], active : false,bounds : null, crashRecover : null, mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",") }; var m = mouse; function mouseMove(e) { var t = e.type; m.x = e.clientX - m.bounds.left; m.y = e.clientY - m.bounds.top; m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey; if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }
    else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; } else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; } else if (t === "mouseover") { m.over = true; } else if (t === "mousewheel") { m.w = e.wheelDelta; } else if (t === "DOMMouseScroll") { m.w = -e.detail; } if (m.callbacks) { m.callbacks.forEach(c => c(e)); } if((m.buttonRaw & 2) && m.crashRecover !== null){ if(typeof m.crashRecover === "function"){ setTimeout(m.crashRecover,0);}}
    e.preventDefault(); } m.updateBounds = function(){ if(m.active){ m.bounds = m.element.getBoundingClientRect(); }

    shell
    } m.addCallback = function (callback) { if (typeof callback === "function") { if (m.callbacks === U) { m.callbacks = [callback]; } else { m.callbacks.push(callback); } } else { throw new TypeError("mouse.addCallback argument must be a function"); } } m.start = function (element, blockContextMenu) { if (m.element !== U) { m.removeMouse(); } m.element = element === U ? document : element; m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu; m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } ); if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); } m.active = true; m.updateBounds(); } m.remove = function () { if (m.element !== U) { m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } ); if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);} m.element = m.callbacks = m.contextMenuBlocked = U; m.active = false; } } return mouse;

    })();

    resizeCanvas(); mouse.start(canvas,true); onResize() var lastTime = null; window.addEventListener("resize",resizeCanvas); function clearCTX(){ ctx.setTransform(1,0,0,1,0,0); // reset transform ctx.globalAlpha = 1; // reset alpha ctx.clearRect(0,0,w,h); // though not needed this is here to be fair across methods and demonstrat flicker }

    function dualUpdate(){ if(!dualTimersActive) { dualTimersActive = true; hdl1 = setInterval( clearCTX, 1000/60); hdl2 = setInterval(() => display(10), 1000/60); } } function timerUpdate(){ timer = performance.now(); if(!lastTime){ lastTime = timer; } var time = (timer-lastTime) / (1000/60); lastTime = timer;
    setTimeout(updateMethods[displayMethod],1000/60); clearCTX(); display(10*time); } function updateRAF(){ clearCTX(); requestAnimationFrame(updateMethods[displayMethod]); display(10);
    } function updateRAFTimer(timer){ // Main update loop

    shell
    clearCTX(); requestAnimationFrame(updateMethods[displayMethod]); if(!timer){ timer = 0; } if(!lastTime){ lastTime = timer; } var time = (timer-lastTime) / (1000/60); display(10 * time); lastTime = timer;

    }

    displayMethod = 1; var updateMethods = [timerUpdate,updateRAF,dualUpdate,updateRAFTimer,timerUpdate] updateMethodsdisplayMethod;

    /** SimpleFullCanvasMouse.js end **/

运行代码片段Hide results

展开片段

2024年6月29日 12:07 回复

你的答案