由requestAnimationFrame谈浏览器渲染优化

1.宏任务队列,每次只会执行队列内的一个任务。

回到我们上面说的代码:

在three.js里,requestAnimationFrame主要用在渲染器renderer里,作为优化动画的解决方案。当然在js animation中也需要用到requestAnimationFrame。在谈此之前,我们就three.js的应用场景,来简单介绍一下动画的相关概念。

理想情况下,页面会以 60 帧每秒的帧率来运行,但实际上每秒绘制多少帧是由多个因素决定的,下面举一些例子:

requestAnimationFrame也属于执行是异步执行的方法,但我任务该方法既不属于宏任务,也不属于微任务。按照MDN中的定义:

window.requestAnimationFrame() 方法告诉浏览器您希望执行动画,并请求浏览器调用指定的函数在下一次重绘之前更新动画。该方法将在重绘之前调用的回调作为参数。

在一个事件循环内,各个队列有以下特性:

我们知道 js 是单线程执行的,那么异步的代码 js 是怎么处理的呢?例如下面的代码是如何进行输出的:

当然,当 FPS 足够大(比如达到 60),再增加帧数 人眼也不会感受到明显的变化,反而相应地就要消耗更多资源(比如电影的胶片就需要更 长了,或是电脑刷新画面需要消耗计算资源等等)

events: 点击事件、键盘事件、滚动事件等macro: 宏任务,如setTimeoutmicro: 微任务,如PromiserAF:requestAnimationFrameLayout: CSS 计算,页面布局Paint: 页面绘制rIC:requestIdleCallback

在 js 中,任务分为宏任务和微任务,这两个任务分别维护一个队列,均采用先进先出的策略进行执行!同步执行的任务都在宏任务上执行。

参考文档


  • requestAnimationFrame
  • How JavaScript Timers Work

上面的函数假定了浏览器以帧率 60 来运行,但当帧率达不到的时候,2 帧之间回调可能执行了多次,也可能一次都不执行,简称掉帧。

宏任务主要有:script、setTimeout、setInterval、I/O、UI 交互事件、postMessage、MessageChannel、setImmediate。

setInterval 与setTimeout

一般做动画而言,我们第一想到的就是使用setInterval或者setTimeout来实现,如下:

function animate() {
    // ...
}
setInterval(animate, 200)

or

function animate(){
    setTimeout(animate,200)
}
animate()

由于大部分屏幕刷新的频率是60HZ,所以我们要做的就是尽量去让帧率达到60fps。

//1000ms/60 = 16.7ms,故约等于17
setInterval(animate,17)

然后,setInterval与setTimeout所设定的时间,并不一定按照间隔来执行。由于浏览器是单线程的缘故,事件都是按异步队列执行,如果执行setTimeout/setInterval时,有大量的异步事件在等待执行,浏览器线程只能让其等待,这样delay肯定时大于所设置的时间。

出现的问题:

  1. 浏览器依然在执行一些不必要的动画,或者异步事件,尽管chrome会对setInterval以及setTimout在1fps做节流处理,但其他浏览器并没有
  2. setTimeout只会在浏览器想要更新的时候更新,而不会考虑计算机是否能够更新,这就意味着当你在重绘整个屏幕的时候,浏览器不得不重绘动画,此时当你的动画帧率跟屏幕重绘得帧率不同步时,于是会耗费更多的电量,这就意味着高CPU使用率。
  3. 另一个要考虑的是多个元素立即发生的运动。一个解决方法是将所有动画逻辑放到同一个间隔以此来解决可能的动画调用,即使特定元素可能不需要当前帧的任何动画

事件循环和上面 4 个名词的基本概念在此不再啰嗦了,我们着重看下它们之间的关系。浏览器是一个 UI 系统,所有的操作最终都会以页面的形式展现,而页面的基本单位是帧。一帧中可能包括的任务有下面几种类型。

执行 log,输出 1; 遇到 setTimeout,将回调的代码 log添加到宏任务中等待执行; 执行 console.log添加到微任务中; 执行 log,输出 5; 遇到 setTimeout,将回调的代码 log添加到宏任务中; 宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,存在一个微任务 log,执行输出 4; 取出下一个宏任务 log执行,输出 2; 宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,不存在; 取出下一个宏任务执行,执行 log添加到微任务中; 宏任务执行完毕,存在一个微任务 log,执行输出 7;

动画的本质是利用了人眼的视觉暂留特性,快速地变换画面,从而产生物体在运动的假象。而对于 Three.js 程序而言,动画的实现也是通过在每秒中多次重绘画面实现的。

来自:

console.log;setTimeout { console.log;new Promise { console.log; resolve.then { console.log;setTimeout { new Promise { console.log; resolve.then { console.log;

requestAnimationFrame这个API,可能很多人都听过,但并没有真正用过。MDN上的解释是:

Promise,setTimeout,rAF和rIC对应 4 种队列:微任务队列、宏任务队列、animation 队列和 idle 队列。

同时,这段代码的输出很有意思:

requestAnimationFrame

为了解决上述问题requestAnimationFrame产生了

function animate(){
    requestAnimationFrame(animate)
}
animate()
//requestAnimationFrame(animate, element)   //可以定义当前节点

requestAnimationFrame的帧率取决于你的浏览器以及计算机,但一般来说都是60fps。requestAnimationFrame关键的就是他只是请求浏览器在下一次可以获得的机会去展示一帧画面,而不是在一个已经规划好的间隔。也就是说浏览器能够根据页面加载,元素显示,电池的状态来选择requestAnimationFrame的性能。

另外一个requestAnimationFrame的优点是它能够将所有的动画都放到一个浏览器重绘周期里去做,这样能保存你的CPU的循环次数,让你的设备存活时间更长。

当然在用requestAnimationFrame设置动画后,当页面出现新的tab后,动画也会停止,从而减少计算机的开销。

下面是requestAnimationFrame的polyfill

(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
                                   || window[vendors[x]+'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
              timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };

    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());

通过以上polyfill可知,requestAnimationFrame可以用setTimeout来改写,但注意到所有的callback的执行时间都控制在16ms以内,这也就说明了,requestAnimationFrame每次的执行都是在页面刷新频率以内的。

当然,我们也可以自己来控制帧率

var fps = 15;
function animate() {
    setTimeout(function() {
        requestAnimationFrame(animate);
    }, 1000 / fps);
}

当然还有更复杂的办法

var time;
function animate() {
    requestAnimationFrame(animate);
    var now = new Date().getTime(),
        dt = now - (time || now);

    time = now;

    // 比如更新x的位置:
    this.x += 10 * dt;
    // 每毫秒增加10个单位
}

那么我们如何在实际开发中用requestAnimationFrame来优化呢?我们假设有这样的应用场景,如果页面要加载上千甚至上万张图片(或者说是li),我们模拟的就是成千上万个dom。
分析:出现卡顿感的主要原因是每次循环都会修改 DOM 结构,考虑上万张图片,用户不会立即看到,所以我们可以缩短循环次数,并且减少DOM操作来进行优化。

  • 减少操作DOM,我们可以使用DocumentFragment
  • 减少循环时间,使用分治的思想,把30000个li分批次插入到页面中,每次插入的时机是在页面重新渲染之前

下面是完整的代码示例:(在这里用背景颜色代替图片)

<ul id="js-list"></ul>

ul#js-list{
  padding:0px;
  display:flex;
  flex-wrap:wrap
}
li{
  list-style:none;
  text-align:center;
  line-height:50px;
  width:50px;
  height:50px;
  border:1px solid #000;
}

(() => {
    const ndContainer = document.getElementById('js-list');
    if (!ndContainer) {
        return;
    }

    const total = 30000;
    const batchSize = 4; // 每批插入的节点次数,越大越卡
    const batchCount = total / batchSize; // 需要批量处理多少次
    let batchDone = 0;  // 已经完成的批处理个数

    var getRandomColor = function(){
      return '#'+Math.floor(Math.random()*16777215).toString(16);
    }
    function appendItems() {
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < batchSize; i++) {
            const ndItem = document.createElement('li');
            ndItem.innerText = (batchDone * batchSize) + i + 1;
            ndItem.style.backgroundColor = getRandomColor()
            fragment.appendChild(ndItem);
        }

        // 每次批处理只修改 1 次 DOM
        ndContainer.appendChild(fragment);

        batchDone += 1;
        doBatchAppend();
    }

    function doBatchAppend() {
        if (batchDone < batchCount) {
            window.requestAnimationFrame(appendItems);
        }
    }

    // kickoff
    doBatchAppend();

    ndContainer.addEventListener('click', function (e) {
        const target = e.target;
        if (target.tagName === 'LI') {
            alert(target.innerHTML);
        }
    });
})();

实现的效果如图DEMO

以上感谢王仕军老师提供的思想跟方法。

其实requestAnimationFrame也运用到了react fiber跟angular中,本文在这里不做详细讲解,后期会针对React Fiber与requestAnimayonFrame再做一次深入探究

二、事件循环与帧

function B() { console.log.then { console.log}console.log;

为了衡量画面切换速度,引入了每秒帧数 FPS(Frames Per Second)的概念,是指每秒画 面重绘的次数。(这也是在游戏中经常遇到的FPS,打过lol的都知道,一般FPS越高,画面会越流畅)FPS 越大,则动画效果越平滑,当 FPS 小于 20 时,一般就能明显感受到 画面的卡滞现象。

4.idle 队列,每次只会执行一个任务。任务完成后会检查是否还有空闲时间,有的话会继续执行下一个任务,没有则等到下次有空闲时间再执行。需要注意的是此队列中的任务也有可能阻塞页面,当空闲时间用完后任务不会主动退出。如果任务占用时间较长,一般会将任务拆分成多个阶段,执行完一个阶段后检查还有没有空闲时间,有则继续,无则注册一个新的 idle 队列任务,然后退出当前任务。React Fiber就是用这个机制。但最新版的React Fiber已经不用rIC了,因为调用的频率太低,改用rAF了

这 4 步构成了一个事件的循环检测机制,即我们所称的eventloop。

function animation() { console.log('time: ', +new Date()); requestAnimationFrame(animation);}animation();

因此,最终的输出顺序为:1, 3, 5, 4, 2, 6, 7;

再来谈谈空闲时间怎么理解。假设在 1 秒内有 3 帧需要渲染:

我们在Promise.then实现一个稍微耗时的操作,这个步骤看起来会更加地明显:

欢迎关注我的公众号 睿Talk ,获取我最新的文章:

前言

function loop() { Promise.resolve().then(loop);}loop();
console.log;setTimeout { console.log;new Promise { console.log; resolve.then { console.log;setTimeout { new Promise { console.log; resolve.then { console.log;

本文介绍了 4 种队列的执行顺序和每个队列的特性,它们是:宏任务队列、微任务队列、animation 队列和 idle 队列。实际应用时可以根据它们各自的特点分配不同的任务。

Promise.then中,先生成一个500万随机数的数组,然后对这个数组进行排序。运行这段代码可以发现:马上会输出1,稍等一会儿才会输出3,然后再输出2。不论等待多长时间输出3,2一定会在3的后面输出。这也就印证了eventloop中的第3步操作,必须等所有的微任务执行完毕后,才开始下一个宏任务。

2.微任务队列,每次会执行队列里的全部任务。假设微任务队列内有 100 个 Promise,它们会一次过全部执行完。这种情况下极有可能会导致页面卡顿。如果在微任务执行过程中继续往微任务队列中添加任务,新添加的任务也会在当前事件循环中执行,很容易造成死循环, 如:

3. requestAnimationFrame

第一帧,由于宏任务占用了大量的时间,没有空闲时间。第二帧,rAF占用的时间不多,有大量的空闲时间第三帧,浏览器事件占用的时间不多,有大量的空闲时间

其实,async-await 只是 Promise+generator 的一种语法糖而已。上面的代码我们改写为这样,可以更加清晰一点:

Promise,setTimeout,requestAnimationFrame,requestIdleCallback这几个概念相信很多人都很熟悉了,最近在看React Fiber源码的时候又对它们有了更深一层的认识,在此分享一下。下文将用rAF代表requestAnimationFrame,rIC代表requestIdleCallback。

我们要记住最重要的两点:js是单线程和eventloop的循环机制。

五、总结

requestAnimationFrame是GUI渲染之前执行,但在微服务之后,不过requestAnimationFrame不一定会在当前帧必须执行,由浏览器根据当前的策略自行决定在哪一帧执行。

setTimeout(()=console.log('setTimeout'), 0);Promise.resolve().then(()=console.log('promise'));requestAnimationFrame(()=console.log('animation'));requestIdleCallback(()=console.log('idle'));// 执行结果大多数情况下是: promise, animation, setTimeout, idle// 少数情况是:promise, setTimeout, animation, idle

在不运行的情况可以先猜测下最终的输出,然后展开我们要说的内容。

3.animation 队列,跟微任务队列有点相似,每次会执行队列里的全部任务。但如果在执行过程中往队列中添加新的任务,新的任务不会在当前事件循环中执行,而是在下次事件循环中执行。

2. async-await

四、队列特性

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

时间: 2019-05-13阅读: 232标签: 队列

从上面的代码中也能看到 Promise.then 中的代码是属于微服务,那么 async-await 的代码怎么执行呢?比如下面的代码:

rAF会保证注册的回调在下次渲染页面之前执行,且只会执行一次。另外,当页面处于不可见状态时,rAF会自动停止执行,以节省系统资源。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。

所以在制作动画的时候,我们不能预设浏览器的帧率,正确的做法是通过rAF注册回调, 由浏览器来控制动画调用时机:

1. 宏任务与微任务

function animation() { console.log('time: ', +new Date()); setTimeout(animate, 1000 / 60);}animation();

这样我们就能明白输出的先后顺序了: 1, 0.4793526730678652, 2, 1557830834679;

从上面的例子可以看出,页面的帧率不是固定的,是会动态变化的。当某一帧的任务占用大量时间的时候,会影响到下一帧的执行。那么谁来调节帧率呢?显然只能依靠浏览器自身。作为开发者的我们是无法准确预知回调什么时候执行的。比如:

从宏任务的头部取出一个任务执行; 执行过程中若遇到微任务则将其添加到微任务的队列中; 宏任务执行完毕后,微任务的队列中是否存在任务,若存在,则挨个儿出去执行,直到执行完毕; GUI 渲染; 回到步骤 1,直到宏任务执行完毕;

三、执行顺序

console.log;var start = Date.now();setTimeout { console.log;setTimeout { console.log - start);}, 400);Promise.resolve { var sum = function { return Number; } var res = []; for(var i=0; i<5000000; i++) { var a = Math.floor; var b = Math.floor; res.push; } res = res.sort

与rAF类似,rIC的执行时机是由浏览器控制的,能更好的保证体验,优化性能。一般优先级高的任务(如 UI 更新)会放在rAF队列,优先级低的任务(如日志上传)会放rIC。

微任务主要有:Promise.then、 MutationObserver、 process.nextTick。

一个加载完成的静态页面,当用户没有进行交互的情况下,页面不需要重绘,帧率为 0。快速滚动页面的时候,可视区域的内容不断发生变化,浏览器会尽可能快的重绘页面,理想帧率为 60。假设页面有一个注册了回调的按钮,回调执行需要 500 毫秒。当点击按钮后再快速滚动页面,头 500 毫秒页面是卡住动不了的,后 500 毫秒会尽可能快的重绘页面,这时候理想帧率为 30。当使用rAF制作动画的时候,浏览器会尽可能快的重绘页面,桌面浏览器可能是 60 帧,移动浏览器可能是 30 帧。

setTimeout { console.log - start); // 4, 1380 电脑状态的不同,输出的时间差也不一样}, 400);

一、前言

依据我们多年编写 ajax 的经验:js 应该是按照语句先后顺序执行,在出现异步时,则发起异步请求后,接着往下执行,待异步结果返回后再接着执行。但他内部是怎样管理这些执行任务的呢?

微任务队列会在 JS 运行栈为空的时候立即执行。animation 队列会在页面渲染前执行。宏任务队列优先级低于微任务队列,一般也会比 animation 队列优先级低,但不是绝对。idle 队列优先级最低,当浏览器有空闲时间的时候才会执行。

function A() { return Promise.resolve;}async function B() { console.log; let now = await A;}console.log;

本来要设定的是400ms后输出,但因为之前的任务耗时严重,导致之后的任务只能延迟往后排。也能说明,setTimeout和setInterval这种操作的延时是不准确的,这两个方法只能大概将任务400ms之后的宏任务中,但具体的执行时间,还是要看线程是否空闲。若前一个任务中有耗时的操作,或者有无限的微任务加入进来时,则会阻塞下一个任务的执行。

4. 总结

本文由澳门威斯尼人平台登录发布于Web前端,转载请注明出处:由requestAnimationFrame谈浏览器渲染优化

相关阅读