由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閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容

  • 在瀏覽器渲染過程與性能優化一文中(建議先去看一下這篇文章再來閱讀本文),我們了解與認識了瀏覽器的關鍵渲染路徑以及如...
    SylvanasSun閱讀 4,689評論 1 5
  • 前言 本文主要參考w3c資料,從底層實現原理的角度介紹了requestAnimationFrame、cancelA...
    Bruce_zhuan閱讀 1,543評論 0 3
  • AJax 優化 緩存 Ajax 請求盡量使用GET, 僅取決于cookie數量 Cookie 優化 減少Cooki...
    KeKeMars閱讀 9,367評論 5 89
  • 一:在制作一個Web應用或Web站點的過程中,你是如何考慮他的UI、安全性、高性能、SEO、可維護性以及技術因素的...
    Arno_z閱讀 1,200評論 0 1
  • 主題:夏天最想干的事兒 金雨(女):減肥 美白 王瑞(女):去有樹的地方散步 金雯(女):我是扇個扇子坐在陽臺上聽...
    王子堂堂閱讀 204評論 1 3