前端交互動畫優化

原文連接:bluest.mecss

 

前端優化是個很普遍的命題,鋪開去得出本書了(事實上我也沒那本事),實際上市面上也有不少相關的書籍。動畫與交互上的性能問題最容易被察覺,特別是在機能較低的移動端。因爲本身有過一段移動開發的經歷,較爲關注這塊且做爲一個愛拾人牙慧的切圖狗,現將一些他人成熟的優化方法總結以下:html

固然,全部的優化都是有場景,請根據實際的場景去選擇最優的方案使用。前端

 

DOM 相關

DOM 天生就慢,以下比喻就很形象的解釋了這樣的關係。git

把 DOM 和 js(ECMAScript)各自想象爲一座島嶼,它們之間用收費橋進行鏈接。ECMAScript 每次訪問 DOM,都要途徑這座橋,並交納「過橋費」。訪問 DOM 的次數越多,費用也就越高。github

最基本的優化思路就是優化 DOM 的讀寫操做。web

減小對 DOM 元素讀操做:

緩存 DOM 引用

獲取 DOM 以後請將引用緩存,不要重複獲取。不少人在使用 jQuery 的時候沒有培養良好的習慣,鏈式調用用起來方便,但有時候會讓人調入忽視緩存 DOM 的陷阱,由於獲取太便捷就不去珍惜了,果真被偏心的就會有恃無恐。後端

var render = (function() {
    // get DOM 
    var domCache = document.querySelector("dom");
    
    return function() {
        // do something...
        domCache.style.width =  '100px';
        domCache.style.height = '100px';
        // ....
    }
})();

緩存 DOM 的屬性

思路同上,在獲取初始值後而且已知變化量,直接經過計算得知元素變化後的值並緩存在內存中,避免將結果值使用 DOM 屬性進行存儲。能夠減小不少沒必要要的 DOM 讀取操做,特別是某些屬性還會引起瀏覽器迴流(這些屬性下文會說起)。這在用 JavaScript 控制一些物體位置變化的時候比較容易忽略。jQuery 時代,人們習慣於將數據保存在 DOM 元素上,卻不知這將引起性能問題,我曾今就犯過相似的錯誤,致使一個移動端上的賽車遊戲性能低下。瀏覽器

// bad
var dom = document.querySelector("dom");
var step = 1;

var timer = window.setInterval(function () {
    var left = parseInt(dom.style.left);
    if (left >= 200) {
        clearInterval(timer);
    }
    dom.style.left = (left +1) + 'px';
}, 1000 / 60);

// good
var dom = document.querySelector("dom");
var step = 1;
var left = parseInt(dom.style.left);

var timer = window.setInterval(function () {
    if (left >= 200) {
        clearInterval(timer);
    }
    left++;
    dom.style.left = left + 'px';
}, 1000 / 60);

還有常見的就是緩存 HTMLCollection 的 length,HTMLCollection 還有一個很重要的特性就是它是根據頁面的狀況動態更新的,若是你更新的頁面那麼它的內容也會發生變化,下面的代碼會是無限循環。緩存

var divs = document.getElementsByTagName("div") ;
for(var i = 0 ; i < divs.length ; i ++){
    document.body.appendChild(document.createElement("div")) ;
}

減小 DOM 的寫操做

記錄上次結果與現有結果 Diff, 若有變化才進行寫操做,去除沒必要要的寫操做。app

var dom = document.querySelector('#dom');
var lastVal =  null;
var currVal = null;

if (lastVal !== currVal) {
    dom.style.someAttr = currVal;
}

避免循環操做 DOM 元素

循環中操做 DOM,每次循環都會產生一次讀操做與寫操做,因此咱們的優化思路是將循環結果緩存起來,循環結束後統一操做能節省不少讀寫操做。

合併屢次寫操做

// bad
for (var i = 0; length < 100; i++) {
    // 一次 get,一次 set
    document.getElementById('text').innerHTML += `text${i}`
}

// better
var html = '';

for (var i = 0; length < 100; i++) {
    html += `text${i}`
}

document.getElementById('text').innerHTML = html;

使用 documentFragment

另外 documentFragment 也可達到這樣的目的,由於文檔片斷存在於內存中,並不在 DOM 樹中,因此將子元素插入到文檔片斷時不會引發頁面迴流。所以,使用文檔片斷 document fragments 一般會起到優化性能的做用。

var fragment = document.createDocumentFragment();

for (var i = 0; length < 100; i++) {
    var div = document.createElement('div');
    div.innerHTML = i;
    fragment.appendChild(div);
}

document.body.appendChild(fragment)

至於上文中 innerHTML 與 fragment 誰更快,請看這裏,有此文還引伸出新的優化規則:優先使用 innerHTML(甚至是更好地 insertAdjacentHTML) 與 fragment

迴流(reflow)與重繪(repaint)

若是瞭解過瀏覽器的渲染原理,咱們知道,重繪和迴流的性能消耗是很是嚴重的,破壞用戶體驗,形成UI卡頓。迴流也叫重排,迴流必定會引發重繪,重繪不必定會觸發迴流。觸發瀏覽器迴流與重繪的條件有:

  • 添加或者刪除可見的DOM元素
  • 元素位置改變
  • 元素尺寸改變
  • 元素內容改變
  • 頁面渲染初始化
  • 瀏覽器窗口尺寸改變,字體大小改變,頁面滾動

咱們的優化思路是減小甚至避免觸發瀏覽器產生迴流與重繪。

避免一些引發瀏覽器迴流的屬性

當獲取一些屬性值時,瀏覽器爲取得正確的值也會發生重排,這些屬性包括:

  • Element:
    • offsetTopoffsetLeftoffsetWidthoffsetHeight
    • scrollTopscrollLeftscrollWidthscrollHeight
    • clientTopclientLeftclientWidthclientHeight
  • Frame, HTMLImageElement:
    • heightwidth
  • Range:
    • getBoundingClientRect(),
    • getClientRects()
  • SVGLocatable:

    • computeCTM()
    • getBBox()
  • SVGTextContent:

    • getCharNumAtPosition()
    • getComputedTextLength()
    • getEndPositionOfChar()
    • getExtentOfChar()
    • getNumberOfChars()
    • getRotationOfChar()
    • getStartPositionOfChar()
    • getSubStringLength()
    • selectSubString()
  • SVGUse:

    • instanceRoot
  • window:

    • getComputedStyle()
    • scrollBy()scrollTo()scrollXscrollY
    • webkitConvertPointFromNodeToPage()webkitConvertPointFromPageToNode()

更全面的屬性請訪問這個Gist

display:none的元素上進行操做

若是 DOM 元素上須要進行不少操做,可讓該 DOM 元素從 DOM 樹中"離線"——display:none,等操做完畢後再」上線「取消display:none。這樣能去除在操做期間引起的迴流與重繪。

操做 cloneNode

也能夠將當前節點克隆一份,操做克隆節點,操做完畢以後再替換原節點。

瀏覽器優化

重排和重繪很容易被引發,並且重排的花銷也不小,若是每句 JavaScript 操做都去重排重繪的話,瀏覽器可能就會受不了。因此不少瀏覽器都會優化這些操做,瀏覽器會維護一個隊列,把全部會引發重排、重繪的操做放入這個隊列,等隊列中的操做到了必定的數量或者到了必定的時間間隔,瀏覽器就會 flush 隊列,進行一個批處理。這樣就會讓屢次的重排、重繪變成一次重排重繪。

var dom = document.querySelector("#dom");

// 觸發兩次 layout
var newWidth = dom.offsetWidth + 10;   // Read  
aDiv.style.width = newWidth + 'px';     // Write  
var newHeight = dom.offsetHeight + 10; // Read  
aDiv.style.height = newHeight + 'px';   // Write

// 只觸發一次 layout
var newWidth = dom.offsetWidth + 10;   // Read  
var newHeight = dom.offsetHeight + 10; // Read  
aDiv.style.width = newWidth + 'px';     // Write  
aDiv.style.height = newHeight + 'px';   // Write

一次性修改元素

每次修改 DOM 元素,均可能引發瀏覽器的迴流與重繪,儘量去較少改變次數,這與上文優化 DOM 讀寫思路重合再也不贅述。

經過樣式去改變元素樣式
// bad
var dom = document.getElementById('dom');
dom.style.color = '#FFF';
dom.style.fontSize = '12px';
dom.style.width = '200px';

上述例子每次修改 style 屬性後都會觸發元素的重繪,若是修改了的屬性涉及大小和位置,將會致使迴流。因此咱們應當儘可能避免屢次爲一個元素設置 style 屬性,應當經過給其添加新的 CSS 類,來修改其樣式。

<!--better-->
<style>
.my-style {
    color: #FFF;
    font-size: 12px;
    width: 200px;
}
</style>

<script>
    var dom = document.getElementById('dom');
    dom.classList.add('my-style');
</script>
cssText

同上文優化思路,用cssText也可達到相似目的。

var dom = document.getElementById('dom');
 dom.style.cssText = 'color: #FFF;font-size: 12px;width: 200px;'

簡化 DOM 結構

首先每一個 DOM 對象的都會佔據瀏覽器資源,佔據的資源與數量成正相關。另外,DOM 結構越深,最裏面 DOM 元素的變化可能引起的祖先 DOM 數量就越多。

使用場景例如大量數據表格的展現,幾萬個 DOM 就能把瀏覽器卡得不要不要的甚至直接奔潰。我曾經遇到這樣真實的案例,後在保持後端接口不變的狀況下,採用前端假分頁解決。

DOM 事件優化

使用事件委託或事件代理

使用事件代理與每一個元素都綁定事件相比,可以節省更多的內存。固然還有另外的好處,就是新增長假的 DOM 元素也無需綁定事件了,這裏不詳述。

截流函數

首先這樣場景下,在頁面滾動的時候需根據頁面滾動位置作一些操做,可是 scroll 事件觸發過於頻繁,致使綁定的事件執行頻率過高開銷太大。咱們就須要採起一些措施來下降事件被執行的頻率。

節流實際上就下降函數觸發的頻率。

let throttle = (func, wait) => {
    let context, args;
    let previous = 0;

    return function () {
        var now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    };
};

防抖函數

說道節流,不得不提防抖,相交於節流的下降觸發的頻率,防抖函數其實是延後函數執行的時機,通常狀況下,防抖比截流更節省性能。

let debounce = (func, wait) => {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;
        clearTimeout(timeout);
        timeout = setTimeout(function () {
            func.apply(context, args)
        }, wait);
    };
};

使用場景例如一個輸入框的實時搜索,對用戶而言其實想要輸入的關鍵詞是輸入完成的最終結果,而程序須要實時針對用戶輸入的無效關鍵詞進行響應,這無疑是種浪費。咱們須要

CSS

文檔流中元素樣式改變可能觸發瀏覽器迴流,被影響的 DOM 樹越大,須要重繪的時間就越長,也就可能致使性能問題。CSS Triggers 就列舉了會引起瀏覽器,迴流與重繪的屬性。

使用定位讓元素脫離文檔流

使用定位讓元素脫離文檔流,引起迴流重繪的 DOM 樹範圍被大大縮小。

.selector {
    position: fixed;
    // or
    position: absolute; 
}

使用 transform 與 opacity

transform 和 opacity 保證了元素屬性的變化不影響文檔流、也不受文檔流影響,而且不會形成重繪。

FLTP

FLIP 來源於 First,Last,Invert,Play。FLIP 是將一些開銷高昂的動畫,如針對 widthheightleft 或 top 的動畫,映射爲 transform 動畫。經過記錄元素的兩個快照,一個是元素的初始位置(First – F),另外一個是元素的最終位置(Last – L),而後對元素使用一個 transform 變換來反轉(Invert – I),讓元素看起來還在初始位置,最後移除元素上的 transform 使元素由初始位置運動(Play – P)到最終位置。

觸發 GPU 加速

使用 GPU 硬件加速可使得瀏覽器動畫更加流暢,不過切勿貪杯, GPU 加速是損耗硬件資源爲代價的,會致使移動端設備續航能力的下降。

.selectror {
    webkit-transform: translateZ(0);
    -moz-transform: translateZ(0);
    -ms-transform: translateZ(0);
    -o-transform: translateZ(0);
    transform: translateZ(0);
}
// 或者
.selector {
    webkit-transform: translate3d(0,0,0);
    -moz-transform: translate3d(0,0,0);
    -ms-transform: translate3d(0,0,0);
    -o-transform: translate3d(0,0,0);
    transform: translate3d(0,0,0);
}

transform 在瀏覽器中可能有一些非預期內的表現,好比閃爍等,可使用以下代碼 hack:

.selector {
    -webkit-backface-visibility: hidden;
    -moz-backface-visibility: hidden;
    -ms-backface-visibility: hidden;
    backface-visibility: hidden;

    -webkit-perspective: 1000;
    -moz-perspective: 1000;
    -ms-perspective: 1000;
    perspective: 1000;
}

will-change

上一種方式實際上是欺騙瀏覽器,達到瀏覽器「誤覺得」須要 GPU 渲染加速,而 will-change 則是很禮貌的告知瀏覽器「這裏會變化,請先作好準備」。不過切勿貪杯,適度使用。

.selector {
    will-change: auto
    will-change: scroll-position
    will-change: contents
    will-change: transform        // Example of <custom-ident> 
    will-change: opacity          // Example of <custom-ident>
    will-change: left, top        // Example of two <animateable-feature>
    
    will-change: unset
    will-change: initial
    will-change: inherit
}

避免複雜的 CSS 選擇器以及 calc

複雜的 CSS 選擇器會致使瀏覽器做大量的計算,咱們應當避免

.box:nth-last-child(-n+1) .title {
  /* styles */
}

避免動畫中使用使用高開銷的CSS屬性

CSS 有些屬性存性能問題,使用它們會致使瀏覽器進行大量計算,特別是在 animation 中,咱們應該謹慎使用,

  • box-shaow
  • background-image:
  • filter
  • border-radius
  • transforms
  • filters

使用 flexbox 佈局替代 浮動佈局

新版 flexbox 通常比舊版 flexbox 或基於浮動的佈局模型更快

Canvas

對於渲染頻率不一致的場景,採用分屏繪製

有些動畫場景好比遊戲中,背景通常變化較遊戲物體運動較少,咱們就能夠把這些跟新頻率較低的物體分離出造成一個更新頻率更低的 Canvas 層。

幀率與幀生成

幀率或幀率是用於測量顯示幀數的量度。測量單位爲「每秒顯示幀數」(Frame per Second,FPS)或「赫茲」,通常來講 FPS 用於描述視頻、電子繪圖或遊戲每秒播放多少幀。
via Wikipedia

上文說了那麼多,其實都是在爲人眼的感覺服務。通常來講電影幀率每秒 24 幀,對通常人而言已算可接受了。可是遊戲與頁面動效追求 60 幀乃至更高,由於電影畫面是預先處理過的,運動畫面中包含了畫面運動信息 —— 也就是咱們人眼看快速運動的物體產生的模糊感,人腦會根據這些模糊感去腦補畫面的運動感。而遊戲或者交互動畫不少是實時繪製出來的,並不包含模糊人腦天然也沒法腦補了,因此對幀率更加苛刻,這也是爲何有些遊戲會有動態模糊彌補遊戲幀率不足來改善遊戲觀感這個選項了。

使用微任務分解大量計算

除了人們關注的幀率,幀生成時間也很重要。假使幀率過關可是生成時間不夠恆定,就容易產生跳幀感,就比如一鍋粥裏的老鼠屎。解決方法就是分解高計算量的操做,維護成任務列表平均分佈到刷新間隔中去執行。謝謝聶俊在講解遊戲刷新率的啓發,玩遊戲也能學知識!哎呀,串場了這是機核的口號~~

使用 requestAnimationFrame

相比 setTimeOut,setInterval 恆定間隔刷新方案,requestAnimationFrame 能充分利用顯示器的刷新機制,與瀏覽器的刷新頻率保持同步,帶來更加流暢的動畫。

另外使用 requestAnimationFrame 頁面處於非激活狀態,動畫也會中止執行,這樣更加節省機器能耗。

Web Worker

JavaScript 是單線程的,大量的計算會阻塞線程,致使瀏覽器丟幀。Web Worker 給 JavaScript 開闢新的線程的能力,咱們能夠將純計算在 Web Worker 中處理。

相關文章
相關標籤/搜索