JavaScript動畫的性能並不亞於CSS動畫。所以,若是使用了現代的動畫庫,例如Velocity,那麼動畫引擎的性能將再也不是app的瓶頸,構成瓶頸的只有代碼。javascript
網絡性能相關css
動畫是瀏覽器運行中資源很是密集的進程,可是有不少技術可以幫助瀏覽器儘量高效地運行。下面會提到這些技術。html
性能影響一切。java
可是對於用戶來講,他們的設備配置良莠不齊,不可能都像開發人員同樣用最新版iPhone。因此,要考慮的就是在低端設備上提供較爲流暢的體驗。還有,有時只考慮設備處於理想負載下的狀況,但實際上,用戶的瀏覽器可能開了不少應用和選項卡,這在必定程度上也須要流暢體驗。編程
一.去除佈局顛簸數組
佈局顛簸,就是DOM操做缺少同步性,是拖垮動畫性能的很主要的因素。對它雖沒有輕鬆的解決辦法,但卻有最佳實踐。能夠繼續來看。瀏覽器
看一下網頁操做是如何進行設置(setting)和獲取(getting)這兩項任務的:能夠設置(更新)或獲取一個元素的CSS屬性。同理,能夠往頁面裏插入新元素或者從頁面裏查詢一組已存在元素。獲取和設置是引起性能開銷的兩個核心瀏覽器進程(另外還有圖形渲染)。能夠這樣來想這個問題:在爲元素設置了新屬性之後,瀏覽器必須計算此次更改所產生的後續影響。例如,改變一個元素的寬度會致使一系列連鎖反應;它的父級元素、兄弟元素和子元素的寬度根據各自的CSS屬性也要調整。
由設置和獲取的交替而致使的UI性能下降被稱爲佈局顛簸。儘管瀏覽器已經爲頁面佈局的從新計算進行了高度優化,但因爲佈局顛簸,這些優化的效果大打折扣。例如,瀏覽器能夠輕易地將同一時間的一系列獲取操做優化成一個單一的、流暢的操做,這是由於瀏覽器在第一次獲取以後能夠緩存頁面的狀態,而後在後續每次獲取操做時,參考那個狀態。可是,若是反覆執行了獲取以後又執行設置,就會讓瀏覽器去作許多繁重的工做,由於設置所作的更改會不斷地使其緩存失效。緩存
當佈局顛簸在動畫循環中出現的時候,對性能的影響更爲厲害。假設一個動畫循環力求達到60幀每秒,這是人眼感知平滑運動的最低值。這意味着在動畫循環中,每個tick都必須在16.7毫秒(1秒/60tick≈16.67毫秒)內完成。佈局顛簸很容易致使每一個tick超過這個時限。最終結果固然就是動畫變得卡頓。儘管有些動畫引擎,例如Velocity.js,在其動畫循環中爲減小布局顛簸進行了優化,但還要小心在你本身的循環中避免出現佈局顛簸,例如在setInterval()或自調用的setTimeout()代碼裏面。性能優化
解決:網絡
方法就是把DOM的設置和獲取的操做分別集合在一塊兒。如下代碼會致使佈局顛簸:
// 糟糕的作法 var currentTop = $("element").css("top"); // 獲取 $("element").style.top = currentTop + 1; // 設置 var currentLeft = $("element").css("left"); // 獲取 $("element").style.left = currentLeft + 1; // 設置
若是重寫上述代碼,把查詢放在一塊兒,把設置放在一塊兒,那麼瀏覽器就能夠打包相應的操做,從而減小代碼形成的佈局顛簸的影響:
var currentTop = $("element").css("top"); // 獲取 var currentLeft = $("element").css("left"); // 獲取 $("element").css("top", currentTop + 1); // 設置 $("element").css("left", currentLeft + 1); // 設置
或者:
var currentTop = $("element").css("top"); // 獲取 var currentLeft = $("element").css("left"); // 獲取 $("element").css({ "top": currentTop + 1, "left": currentLeft + 1 }); // 設置
以上所說明的問題常常會在生產代碼中看到,尤爲是當UI操做依賴於元素當前CSS屬性值的時候。
好比你的目的是在單擊按鈕的時候,切換側邊菜單的可見性。要想達到這一效果,你可能會先檢查側邊菜單的display屬性是設置成"none"仍是"block",而後再相應地進行值的替換。檢查display屬性的過程構成一次「獲取」;後續不管是將側邊菜單顯示出來仍是隱藏起來都構成了一次「設置」。
要想優化這種代碼就必須在內存中保留一個變量,每當按鈕點擊時,這個變量跟着更新,而後在切換可見性以前,經過查詢這個變量得知側邊菜單的當前狀態。這樣,「獲取」的過程就徹底省掉了,從而有助於減小設置和獲取交替出現的可能性。另外,除了下降佈局顛簸發生的可能性之外,UI如今還得益於減小了一次頁面查詢。記住:每次設置和獲取對於瀏覽器操做來講都比較消耗性能;設置和獲取次數越少,UI的速度就會越快。許許多多的小改進最終會積累成至關可觀的好處,而這正是本文的潛在主題:儘量多地遵循性能最佳實踐,就能夠儘量少地爲了性能而妥協本身心中理想的動效設計目標,從而實現滿意的頁面。
Jquery元素對象
若是網頁使用了Jquery,實例化Jquery對象也是形成DOM獲取操做的一個因素。
好比:
$("#element").css("opacity", 1);
或者等效的原生JavaScript:
document.getElementById("element").style.opacity = 1;
在jQuery代碼中,由$("#element")返回的值就是一個JEO,即一個包裝了所查詢的原生DOM元素的對象。JEO提供了全部你歡喜的jQuery功能,包括.css()、.animate()等。
原生代碼中,getElementById()返回的是一個沒有包裝過的DOM元素,上面兩種寫法都要求瀏覽器搜索DOM樹,找到想要的元素。這種操做,若是重複屢次,就會影響頁面的性能。
當未被緩存的元素在重複使用的代碼片斷中出現,例如在循環代碼中,對性能的影響就更嚴重了。下面這個例子:
$elements.each(function(i, element) { $("body").append(element); });
each中反覆訪問$(body),會影響性能。再者每次循環都會append()一個元素,致使一次重排版,也會影響性能。
解決這兩個問題的方法,分別是緩存Jquery包裝對象和批量操做DOM:
// 糟糕作法:未緩存JEO $("#element").css("opacity", 1); // …… 一些中間代碼…… // 咱們再次將JEO實例化 $("#element").css("opacity", 0);
緩存Jquery包裝對象:
// 緩存jQuery元素對象,在變量前面加個前綴$用來表示這是個JEO var $element = $("#element"); $element.css("opacity", 1); // …… 一些中間代碼 …… // 咱們複用了緩存的JEO,避免了一次DOM查詢 $element.css("opacity」, 0);
在後面的代碼裏面能夠一樣使用$element.
強制給值:動畫引擎的傳統作法是在動畫的一開始查詢一遍DOM來肯定每一個被設置動畫的CSS屬性的初始值是多少。Velocity經過一種稱爲「強制給值」的功能能夠繞過這一頁面查詢事件。這也是避免佈局顛簸的另外一項技術。經過強制給值,能夠明確地爲動畫設置初始值,從而完全免去了一開始就對頁面進行獲取的操做。強制給定的值做爲第二項被傳入一個數組中,而這個數組替代了本來動畫屬性映射中屬性值的位置。數組中的第一項是你想要設置動畫變更到的最終值。
批量添加DOM
有一種常見的頁面設置操做是在頁面運行時插入新DOM元素。爲頁面添加新元素有不少用途,不過其中最流行的也許就是無限滾動了,它在用戶向下滾動的時候,不斷讓新元素在頁面底部以動畫方式進入視圖。在前面已經知道,每當有一個新元素添加進來,瀏覽器就必須針對全部受到影響的元素進行計算。這是一個相對較慢的過程。所以,當每秒要進行屢次DOM插入時,頁面的性能就會受到顯著影響。幸運的是,當處理多個元素時,若是全部元素是同時插入的,那麼瀏覽器能夠對這個設置的操做進行優化。但不幸的是,做爲開發人員的咱們常常無心識地放棄了這種優化作法,給DOM單獨添加元素。請看下面未優化的DOM插入作法:
// 糟糕的作法 var $body = $("body"); var $newElements = [ "<div>Div 1</div>", "<div>Div 2</div>", "<div>Div 3</div>" ]; $newElements.each(function(i, element) { $(element).appendTo($body); // 其餘代碼 });
以上代碼遍歷了一組元素字符串,這組元素字符串被實例化到jQuery元素對象中。(這麼作沒有什麼性能損失,由於你沒有針對每一個JEO去查詢DOM。)而後,使用jQuery的appendTo()函數將每一個元素插入到頁面中。
問題是這樣的:即便在appendTo()語句後面還有其餘代碼,瀏覽器也不會把這些DOM設置操做壓縮成一個單一的插入操做,由於瀏覽器不能肯定循環之外的異步代碼操做不會在插入操做之間修改DOM狀態。例如,想象這樣一個場景:在每次插入以後都查詢DOM,想要搞清楚究竟有多少元素在頁面上存在:
// 糟糕的作法 $newElements.each(function(i, element) { $(element).appendTo($body); // 輸出body元素有多少個子元素 console.log($body.children().size()); });
瀏覽器沒法將上面的DOM插入優化成一次操做,這是由於代碼明確要求瀏覽器告訴咱們,在下次循環開始以前,究竟存在多少元素。由於瀏覽器每次都要返回正確數值,所以它沒法批量處理後面全部的插入操做。
總之,在循環內部進行DOM元素插入時,每一次插入的操做與其餘都是互相獨立的,所以會形成明顯的性能損失
解決方法就是,不要將一個元素直接插入DOM中,先構建一個完整的DOM集合,而後一次性插入到頁面中去。前面舉的例子能夠優化成:
// 優化後 var $body = $("body"); var $newElements = [ "<div>Div 1</div>", "<div>Div 2</div>", "<div>Div 3</div>" ]; var html = ""; $newElements.each(function(i, element) { html += element; }); $(html).appendTo($body);
上面代碼還有能夠優化的地方,字符串拼接能夠繼續優化。
以上代碼將表明每一個HTML元素的字符串連在一塊兒造成一個主字符串,而後把這個主字符串轉成JEO並一次性添加到DOM上。經過這種作法,瀏覽器獲得明確指示,將全部元素一次性插入,相應的性能也獲得了優化。
避免影響臨近的元素:
提高性能很重要的一點就是要考慮一個元素的動畫對其臨近元素的影響。
例如,若是夾在兩個兄弟元素之間的一個元素寬度縮小,那麼它的兄弟元素的絕對定位就會動態改變,從而保持在動畫元素的旁邊。另外一個例子多是設置嵌入在父元素中的子元素的動畫,而這個父元素並無明肯定義的width和height屬性。相應地,設置子元素的動畫時,父元素的尺寸也會改變,從而確保將子元素徹底包裹在內。實際上,子元素並非惟一被設置動畫的元素,由於它的父元素的尺寸也被設置了動畫。若是這發生在動畫循環裏面,那麼瀏覽器在每次循環時要作的工做就更多了!
有不少CSS屬性,一經改變,就會形成臨近元素尺寸或位置的調整,其中包括:top、right、bottom和left,全部的margin和padding屬性,border厚度,以及width和height尺寸。做爲關心性能的開發人員,須要瞭解設置這些屬性的動畫會給頁面帶來什麼影響。時刻問本身,設置每一個屬性的動畫會怎樣影響臨近元素。若是重寫代碼可以讓你避免元素變化帶來的互相影響,那麼請考慮重寫。事實上,要這麼作有一種簡便方法,繼續看後面的解決辦法!
解決方法:
這種能夠避免影響到臨近元素的解決辦法是儘量設置CSS的transform屬性(translateX、translateY、scaleX、scaleY、rotateZ、rotateX和rotateY)的動畫。transform屬性的特殊之處在於它們將目標元素提高至一個單獨的層,這個層能夠獨立於頁面其餘內容單獨渲染(經過GPU加速提高性能),所以相鄰的元素不會受到影響。例如,在設置一個元素的translateX變更到"500px"的動畫時,元素會向右移動500像素,覆蓋在任何動畫路徑上已經存在的元素的上面。若是在動畫路徑上沒有任何元素(也就是沒有相鄰的元素),那麼使用translateX的效果與設置更慢的left屬性的動畫的效果,在頁面上看起來是同樣的。
因此瀏覽器支持的狀況下,本來這樣:
// 將元素自左側移動500像素 $element.velocity({ left: "500px" });
就能夠寫成這樣:
// 更快:使用translateX $element.velocity({ translateX: "500px" });
top也是相似的:
$element.velocity({ top: "100px" }); // 更快:使用translateY $element.velocity({ translateY: "100px" });
減小併發加載:
當頁面首次加載時,瀏覽器會盡量快地處理HTML、CSS、JavaScript和圖片。所以不出意外,這時候發生的動畫容易發生延遲,它們在努力搶奪瀏覽器有限的資源。因此,儘管在頁面加載序列中添加動畫是顯擺動效設計技巧的好時機,但若是想避免用戶產生網站很慢的第一印象,那麼剋制本身不要這麼作。同理,當許多動畫同時在頁面上發生時,也會出現一個相似的併發性瓶頸,不論它是出如今頁面生命週期中的哪一個階段。在這些狀況下,瀏覽器在同時處理衆多樣式變化的重壓下會喘不過氣來,而後卡頓就發生了。
錯開動畫:
減小並發動畫加載的一個方式是使用Velocity的UI pack中的stagger功能,它會相繼在一組元素的動畫開始前添加指定的延遲時間。例如,要設置一組元素中每一個元素的opacity值變更至1的動畫,而且在動畫開始時間之間相繼添加300毫秒的延遲,代碼可能會是這樣:
$elements.velocity({ opacity: 1 }, { stagger: 300 });
這時候,這些元素再也不是徹底同步執行動畫的,而是在整個動畫序列的開頭,只有第一個元素在執行動畫。而後,在整個序列的結尾,只有最後一個元素執行動畫。你很高效地分散了動畫序列的總工做量,使瀏覽器老是在每一刻作更少的工做,而不是同時執行每一個元素的動畫,讓瀏覽器累得喘不過氣來。另外,在動效設計中使用錯開動畫,一般會獲得較好的審美效果。
多動畫序列:
減小併發加載還有另外一個方法:將多個屬性的動畫拆成多動畫序列。以設置元素的opacity值的動畫爲例。這一般是一個相對輕鬆的操做。可是,若是同時還要設置元素的width和box-shadow屬性的動畫,那麼就會給瀏覽器帶來更多可觀的工做量:會影響更多像素,也要進行更多計算。所以,若是本來動畫像這樣子:
$images.velocity({ opacity: 1, boxShadowBlur: "50px" });
能夠改寫成:
$images .velocity({ opacity: 1 }) .velocity({ boxShadowBlur: "50px" });
這樣瀏覽器就有更少的併發工做要作,由於這些都是一個接一個發生的單獨屬性動畫。注意此處要進行權衡,由於整個動畫序列的持續時間變長了。這對於最終的應用場景而言,也許是好事,也許是壞事。
既然這種優化須要改變你本來對動效設計的想法,那麼這一技巧並不是老是要使用。把它做爲最後的手段吧。若是須要在低端設備上擠出額外的性能,那麼用這種技巧或許合適。其餘狀況下,不要用這種技巧預先優化網站上的代碼,不然的話,最終獲得的將是沒必要要的臃腫且晦澀的代碼。
不用持續響應滾動(scroll)和調整大小(resize)事件
對於高頻事件,最好使用防抖動控制發生頻率。瀏覽器的滾動(scroll)和調整大小(resize)是兩個觸發頻率很是頻繁的事件類型:每當用戶調整或滾動瀏覽器窗口時,瀏覽器都會在每秒內觸發屢次與這些事件相關的回調函數。所以,若是你註冊的回調函數與DOM有交互的話,或者更糟,包含佈局顛簸的話,那麼它們會在滾動或調整大小時帶來巨大的瀏覽器負擔。請看下面的代碼:
// 當滾動瀏覽器窗口時,執行一個行爲 $(window).scroll(function() { // 這裏寫的任何行爲都會在用戶滾動時,每秒鐘觸發屢次 }); // 當瀏覽器窗口的大小改變時,執行一個行爲 $(window).resize(function() { // 這裏寫的任何行爲都會在用戶調整窗口大小時,每秒鐘觸發屢次 });
解決方式就是加防抖動。防抖動就是,定義一個時間間隔,在此時間間隔期間,事件句柄回調將僅會被調用一次。例如,假設你定義了一個250毫秒的反跳間隔,而用戶滾動頁面的總持續時間爲1000毫秒。這時候,進行了防抖動的事件句柄代碼就會相應地僅觸發四次(1000毫秒/250毫秒)。
若是不想本身寫原生js來防抖動,不少庫能夠用,好比的Underscore.js(UnderscoreJS.org),它是一個與jQuery很相近、也提供用於簡化編程的輔助函數的JavaScript庫,這個庫含有debounce函數,你能夠輕鬆地在事件句柄上反覆使用它。
一些代碼是這樣:
// MooTools Function.implement({ debounce: function(wait, immediate) { var timeout, func = this; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } }); // Use it! window.addEvent("resize", myFn.debounce(500))
不過如今部分瀏覽器,如Chrome的最新版本已經自動反跳滾動事件了。
減小圖片渲染:
視頻和圖片是多媒體元素類型,瀏覽器必需要加倍努力渲染才行。要計算非多媒體元素的尺寸屬性是很輕鬆的,可是多媒體元素包含成千上萬的像素數據,要改變它們的大小、尺寸或是從新合成,對瀏覽器而言計算開銷是很大的。設置這些元素的動畫的性能老是比不上設置標準HTML元素(如div、p和table)的動畫的性能,來得理想。另外,鑑於滾動頁面幾乎能夠視爲設置整個頁面的動畫(能夠把滾動頁面視爲設置頁面的top屬性的動畫),在CPU不高的移動設備上,多媒體元素也會形成滾動性能的巨幅降低。
另外,鑑於滾動頁面幾乎能夠視爲設置整個頁面的動畫(能夠把滾動頁面視爲設置頁面的top屬性的動畫),在CPU不高的移動設備上,多媒體元素也會形成滾動性能的巨幅降低。
解決:
不幸的是,除了儘量把簡單的、基於圖形的圖片轉成SVG元素之外,就沒有其餘任何辦法能夠將多媒體內容重構成更快的元素類型。所以,惟一可行的性能優化作法就是減小在頁面上同時顯示和同時設置動畫的多媒體元素總數。注意這裏用到的同時一詞是在強調瀏覽器渲染的客觀狀況:瀏覽器只渲染能夠看到的東西。頁面上看不到的部分(包括包含額外圖片的部分)是不會被渲染的,並且也不會對瀏覽器進程形成額外壓力。所以,有兩種最佳實踐能夠遵循:第一種,若是本來感受在頁面上添不添額外圖片都無所謂的話,那麼選擇不添。要渲染的圖片越少,UI性能就越好。(更不用說更少的圖片給頁面網絡加載時間帶來的正面影響。)
第二種,若是你的UI在同時加載不少圖片到視圖(好比,8幅或以上,根據設備硬件性能而定),考慮不要設置這些圖片的動畫,或者只是簡單地切換每幅圖片的可見性從不可見到可見。這種視覺效果可能並不優雅,要彌補這一點,能夠考慮錯開切換可見性的動畫時間,使圖片一個接一個顯示而不是同時顯示出來,這樣作一般會產生出更精緻的動效設計。
除了img元素,還有其餘的形式,圖片顯示到頁面上的形式。
CSS漸變:漸變其實是圖片的一種。它們不是用圖片編輯器事先生成的,而是根據CSS的樣式定義,在運行時生成的,例如在一個元素的background-image屬性上用了linear-gradient()做爲值。這裏的解決辦法是儘可能選擇純色而非漸變背景。瀏覽器能夠輕鬆優化純色色塊的渲染,可是就像對待圖片同樣,瀏覽器渲染漸變也格外費力,由於漸變的色彩是逐像素變化的。
陰影屬性:漸變有個壞壞的雙胞胎,那就是box-shadow和text-shadow這兩個CSS屬性。它們的渲染跟漸變的渲染大同小異,只不過不是在background-color上,而是在border-color上罷了。更糟糕的是,它們的不透明度還逐漸減小,這要求瀏覽器進行額外的合成工做,由於漸變的半透明部分必須依據動畫元素下面的元素來渲染。這裏的解決辦法跟以前的差很少:若是從樣式表上移除這些CSS屬性後,UI的視覺效果跟以前差很少優秀,那麼寬慰一下本身,放棄以前的方案吧。網站的高性能會反過來回報你的。
這些建議只是建議而已。它們並不是性能最佳實踐,由於你要爲了提升性能而犧牲設計本意。只有當網站性能很糟糕的時候,才考慮使用這些沒有辦法的辦法。
在舊瀏覽器上降級動畫:
IE系列瀏覽器在本國還在普遍被使用,低版本的IE也會佔到必定份額。另外,運行着Android 2.3.x及更早系統的老安卓智能手機比最新一代的Android和iOS設備要慢,但它們依然被普遍使用。相應地,若是你的網站有豐富的動畫和其餘UI互動,那麼就能夠推斷對於這塊用戶來講,網站的運行很糟糕。
解決:
要解決低端設備形成的性能問題有兩種方式:要麼無論三七二十一減小整個網站的動畫;要麼只針對低端設備減小動畫。前者說究竟是一種產品決策,然後者則只是一種能夠輕鬆實施的技術決策,只要使用了全局動畫乘數技術(或Velocity中對應的mock功能)。全
局乘數技術使你可以經過一個變量改變整個網站的動畫時間。所以,這裏的訣竅就是:每當檢測出用戶正在使用性能較弱的瀏覽器,那麼就將乘數設置爲0(或者將$.Velocity.mock設置爲true)。這樣作就能讓整個頁面的動畫都在一個動畫tick(少於16毫秒)中完成:
// 使全部動畫當即完成 $.Velocity.mock = true;
這樣,在性能較差的瀏覽器中,本來的動畫漸變變成樣式的當即修改,會流暢一些。
另外,找到性能門限,在參考設備上測試性能,也相當重要,
參考:《javascript網頁動畫設計》