由requestAnimationFrame谈浏览器渲染优化

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

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

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

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

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

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

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. 另一个要考虑的是多个元素立即发生的运动。一个解决方法是将所有动画逻辑放到同一个间隔以此来解决可能的动画调用,即使特定元素可能不需要当前帧的任何动画

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再做一次深入探究

参考文档


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 在浏览器渲染过程与性能优化一文中(建议先去看一下这篇文章再来阅读本文),我们了解与认识了浏览器的关键渲染路径以及如...
    SylvanasSun阅读 4,643评论 1 5
  • 前言 本文主要参考w3c资料,从底层实现原理的角度介绍了requestAnimationFrame、cancelA...
    Bruce_zhuan阅读 1,519评论 0 3
  • AJax 优化 缓存 Ajax 请求尽量使用GET, 仅取决于cookie数量 Cookie 优化 减少Cooki...
    KeKeMars阅读 9,329评论 5 89
  • 一:在制作一个Web应用或Web站点的过程中,你是如何考虑他的UI、安全性、高性能、SEO、可维护性以及技术因素的...
    Arno_z阅读 1,138评论 0 1
  • 主题:夏天最想干的事儿 金雨(女):减肥 美白 王瑞(女):去有树的地方散步 金雯(女):我是扇个扇子坐在阳台上听...
    王子堂堂阅读 195评论 1 3