前端交互動畫優化

前端交互動畫優化

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

當然,所有的優化都是有場景,請根據實際的場景去選擇最優的方案使用。

DOM 相關

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

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

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

減少對 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 元素上,殊不知這將引發性能問題,我曾今就犯過類似的錯誤,導致一個移動端上的賽車遊戲性能低下。

// badvar 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);// goodvar 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, 如有變化才進行寫操作,去除不必要的寫操作。

var dom = document.querySelector(#dom);var lastVal = null;var currVal = null;if (lastVal !== currVal) { dom.style.someAttr = currVal;}

避免循環操作 DOM 元素

循環中操作 DOM,每次循環都會產生一次讀操作與寫操作,所以我們的優化思路是將循環結果緩存起來,循環結束後統一操作能節省很多讀寫操作。

合併多次寫操作

// badfor (var i = 0; length < 100; i++) { // 一次 get,一次 set document.getElementById(text).innerHTML += `text${i}`}// bettervar 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)

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

迴流(reflow)與重繪(repaint)

如果了解過瀏覽器的渲染原理,我們知道,重繪和迴流的性能消耗是非常嚴重的,破壞用戶體驗,造成UI卡頓。迴流也叫重排,迴流一定會引起重繪,重繪不一定會觸發迴流。觸發瀏覽器迴流與重繪的條件有:

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

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

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

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

  • Element:

    • offsetTopoffsetLeftoffsetWidthoffsetHeight
    • scrollTopscrollLeftscrollWidthscrollHeight
    • clientTopclientLeftclientWidthclientHeight
  • Frame, HTMLImageElement:
  • Range:

    • getBoundingClientRect(),
    • getClientRects()
  • SVGLocatable:
  • SVGTextContent:

    • getCharNumAtPosition()
    • getComputedTextLength()
    • getEndPositionOfChar()
    • getExtentOfChar()
    • getNumberOfChars()
    • getRotationOfChar()
    • getStartPositionOfChar()
    • getSubStringLength()
    • selectSubString()
  • SVGUse:
  • 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");// 觸發兩次 layoutvar newWidth = dom.offsetWidth + 10; // Read aDiv.style.width = newWidth + px; // Write var newHeight = dom.offsetHeight + 10; // Read aDiv.style.height = newHeight + px; // Write// 只觸發一次 layoutvar 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 讀寫思路重合不再贅述。

通過樣式去改變元素樣式

// badvar 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 是將一些開銷高昂的動畫,如針對 widthheightlefttop 的動畫,映射為 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 中處理。


推薦閱讀:

VUE路由許可權驗證
前端構建工具,是幹嘛的?
大廠前端面試題 - 收藏集 - 掘金
React v16.3 版本新生命周期函數淺析及升級方案
2018-02-12 入門前端的必要軟體

TAG:前端開發 | 前端工程師 | 交互設計師 |