原文連接:bluest.mecss
前端優化是個很普遍的命題,鋪開去得出本書了(事實上我也沒那本事),實際上市面上也有不少相關的書籍。動畫與交互上的性能問題最容易被察覺,特別是在機能較低的移動端。因爲本身有過一段移動開發的經歷,較爲關注這塊且做爲一個愛拾人牙慧的切圖狗,現將一些他人成熟的優化方法總結以下:html
固然,全部的優化都是有場景,請根據實際的場景去選擇最優的方案使用。前端
DOM 天生就慢,以下比喻就很形象的解釋了這樣的關係。git
把 DOM 和 js(ECMAScript)各自想象爲一座島嶼,它們之間用收費橋進行鏈接。ECMAScript 每次訪問 DOM,都要途徑這座橋,並交納「過橋費」。訪問 DOM 的次數越多,費用也就越高。github
最基本的優化思路就是優化 DOM 的讀寫操做。web
獲取 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 讀取操做,特別是某些屬性還會引起瀏覽器迴流(這些屬性下文會說起)。這在用 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")) ; }
記錄上次結果與現有結果 Diff, 若有變化才進行寫操做,去除沒必要要的寫操做。app
var dom = document.querySelector('#dom'); var lastVal = null; var currVal = null; if (lastVal !== currVal) { dom.style.someAttr = currVal; }
循環中操做 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
。
若是瞭解過瀏覽器的渲染原理,咱們知道,重繪和迴流的性能消耗是很是嚴重的,破壞用戶體驗,形成UI卡頓。迴流也叫重排,迴流必定會引發重繪,重繪不必定會觸發迴流。觸發瀏覽器迴流與重繪的條件有:
咱們的優化思路是減小甚至避免觸發瀏覽器產生迴流與重繪。
當獲取一些屬性值時,瀏覽器爲取得正確的值也會發生重排,這些屬性包括:
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
clientTop
、clientLeft
、clientWidth
、clientHeight
height
、width
getBoundingClientRect()
,getClientRects()
SVGLocatable:
computeCTM()
getBBox()
SVGTextContent:
getCharNumAtPosition()
getComputedTextLength()
getEndPositionOfChar()
getExtentOfChar()
getNumberOfChars()
getRotationOfChar()
getStartPositionOfChar()
getSubStringLength()
selectSubString()
SVGUse:
instanceRoot
window:
getComputedStyle()
scrollBy()
、scrollTo()
、scrollX
、scrollY
webkitConvertPointFromNodeToPage()
、webkitConvertPointFromPageToNode()
更全面的屬性請訪問這個Gist
display:none
的元素上進行操做若是 DOM 元素上須要進行不少操做,可讓該 DOM 元素從 DOM 樹中"離線"——display:none
,等操做完畢後再」上線「取消display:none
。這樣能去除在操做期間引起的迴流與重繪。
也能夠將當前節點克隆一份,操做克隆節點,操做完畢以後再替換原節點。
重排和重繪很容易被引發,並且重排的花銷也不小,若是每句 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
也可達到相似目的。
var dom = document.getElementById('dom'); dom.style.cssText = 'color: #FFF;font-size: 12px;width: 200px;'
首先每一個 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); }; };
使用場景例如一個輸入框的實時搜索,對用戶而言其實想要輸入的關鍵詞是輸入完成的最終結果,而程序須要實時針對用戶輸入的無效關鍵詞進行響應,這無疑是種浪費。咱們須要
文檔流中元素樣式改變可能觸發瀏覽器迴流,被影響的 DOM 樹越大,須要重繪的時間就越長,也就可能致使性能問題。CSS Triggers 就列舉了會引起瀏覽器,迴流與重繪的屬性。
使用定位讓元素脫離文檔流,引起迴流重繪的 DOM 樹範圍被大大縮小。
.selector { position: fixed; // or position: absolute; }
transform 和 opacity 保證了元素屬性的變化不影響文檔流、也不受文檔流影響,而且不會形成重繪。
FLTP
FLIP 來源於 First,Last,Invert,Play。FLIP 是將一些開銷高昂的動畫,如針對
width
,height
,left
或top
的動畫,映射爲transform
動畫。經過記錄元素的兩個快照,一個是元素的初始位置(First – F),另外一個是元素的最終位置(Last – L),而後對元素使用一個 transform 變換來反轉(Invert – I),讓元素看起來還在初始位置,最後移除元素上的 transform 使元素由初始位置運動(Play – P)到最終位置。
使用 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; }
上一種方式實際上是欺騙瀏覽器,達到瀏覽器「誤覺得」須要 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 }
calc
複雜的 CSS 選擇器會致使瀏覽器做大量的計算,咱們應當避免
.box:nth-last-child(-n+1) .title { /* styles */ }
CSS 有些屬性存性能問題,使用它們會致使瀏覽器進行大量計算,特別是在 animation
中,咱們應該謹慎使用,
box-shaow
background-image:
filter
border-radius
transforms
filters
新版 flexbox 通常比舊版 flexbox 或基於浮動的佈局模型更快
有些動畫場景好比遊戲中,背景通常變化較遊戲物體運動較少,咱們就能夠把這些跟新頻率較低的物體分離出造成一個更新頻率更低的 Canvas 層。
幀率或幀率是用於測量顯示幀數的量度。測量單位爲「每秒顯示幀數」(Frame per Second,FPS)或「赫茲」,通常來講 FPS 用於描述視頻、電子繪圖或遊戲每秒播放多少幀。
via Wikipedia
上文說了那麼多,其實都是在爲人眼的感覺服務。通常來講電影幀率每秒 24 幀,對通常人而言已算可接受了。可是遊戲與頁面動效追求 60 幀乃至更高,由於電影畫面是預先處理過的,運動畫面中包含了畫面運動信息 —— 也就是咱們人眼看快速運動的物體產生的模糊感,人腦會根據這些模糊感去腦補畫面的運動感。而遊戲或者交互動畫不少是實時繪製出來的,並不包含模糊人腦天然也沒法腦補了,因此對幀率更加苛刻,這也是爲何有些遊戲會有動態模糊彌補遊戲幀率不足來改善遊戲觀感這個選項了。
除了人們關注的幀率,幀生成時間也很重要。假使幀率過關可是生成時間不夠恆定,就容易產生跳幀感,就比如一鍋粥裏的老鼠屎。解決方法就是分解高計算量的操做,維護成任務列表平均分佈到刷新間隔中去執行。謝謝聶俊在講解遊戲刷新率的啓發,玩遊戲也能學知識!哎呀,串場了這是機核的口號~~
requestAnimationFrame
相比 setTimeOut
,setInterval
恆定間隔刷新方案,requestAnimationFrame
能充分利用顯示器的刷新機制,與瀏覽器的刷新頻率保持同步,帶來更加流暢的動畫。
另外使用 requestAnimationFrame
頁面處於非激活狀態,動畫也會中止執行,這樣更加節省機器能耗。
JavaScript 是單線程的,大量的計算會阻塞線程,致使瀏覽器丟幀。Web Worker 給 JavaScript 開闢新的線程的能力,咱們能夠將純計算在 Web Worker 中處理。