實現達到60FPS 的高性能交互動畫
原文鏈接: Performant Web Animations and Interactions: Achieving 60 FPS
譯者註:這篇大部分是老生常談,但也稍微有一些新東西呢,要看到最後哦=)
每一個追求交互自然的產品都希望擁有一套順暢的使用流程。但開發者可能會忽略一些細節,導致出現性能糟糕的Web 動畫,不僅會產生「頁面垃圾」(janky),最直接的就是出現頁面卡頓。開發者往往會花大量精力在優化首屏加載,為了幾毫秒錙銖必較,但忽略了頁面交互動畫所帶來的性能問題。
Algolia的每一位同事都很關注用戶體驗,「性能」一定是這個話題裡無法迴避的關鍵部分。動畫性能之於頁面的重要性,就像搜索結果速度之於搜索一樣。
成功的標準
動畫幀率可以作為衡量標準,一般來說畫面在60fps 的幀率下效果比較好。換算一下就是,每一幀要在16.7ms (16.7 = 1000/60) 內完成渲染。因此,我們的首要任務是減少不必要的性能消耗。
越多的幀需要渲染的,意味著有越多的任務需要瀏覽器處理,所以掉幀就出現了,這是達到60fps 的一個絆腳石。如果所有動畫都無法在16.7ms 渲染完畢,不如考慮用略低的30fps 幀率來渲染。
瀏覽器101:像素是怎麼來的
在深入研究之前,我們要先搞清楚一個很重要的問題:瀏覽器是怎麼把代碼轉化成為用戶可見的像素點呢?
首次加載時,瀏覽器會下載並解析HTML,將HTML 元素轉變為一個DOM 節點的「內容樹」(content tree)。除此之外,樣式同樣會被解析生成「渲染樹」 (render tree)。為了提升性能,渲染引擎會分開完成這些工作,甚至會出現渲染樹比DOM 樹更快生成出來。

<首次頁面加載時的call tree>
佈局
渲染樹生成後,瀏覽器會從頁面左上角開始迭代地計算出每個元素尺寸和位置,最終生成佈局。這個過程可能是一氣呵成的,但也可能由於元素的排列導致反复地繪製。元素間的位置關係都緊密相關。為了優化必要的任務,瀏覽器會追踪元素的變化情況,並將這些元素以及它們的子節點標記為'dirty'(臟元素)。但是元素間耦合緊密,任何佈局上的改變代價都是重大的,應該盡量避免。
繪製
生成佈局後,瀏覽器將頁面繪製到屏幕上。這個環節和「佈局」步驟類似,瀏覽器會追踪臟元素,將它們合併到一個超大的矩形區域中。每一幀內只會發生一次重繪,用於繪製這個被污染區域。重繪也會消耗大量性能,能免則免。
複合
最後一步,將所有繪製好的元素進行複合。默認情況下,所有元素將會被繪製到同一個層中;如果將元素分開到不同的複合層中,更新元素對性能友好,不在同一層的元素不容易受到影響。 CPU 繪製層,GPU 生成層。基礎繪圖操作在硬件加速合成中完成效率高。層的分離允許非破壞性的改變,正如你所猜測的, GPU複合層上的改變代價最小性能消耗最少。
激發創造力
一般情況下,更改複合層是相對消耗性能較少的一個操作,所以盡量通過改變opacity和transform的值觸發複合層繪製。看起來好像…我們能做出的效果會很有限,但真的是這樣嗎?要好好開發自己的創造力哦。
變換
「變換」為元素提供了無限的可能性:位置可以改變( translateX , translateY ,或translate3d )、大小也可以通過縮放( scale )改變、還能旋轉、斜切甚至3D變換。就是在某些場景下,開發者需要換一種思考方式,通過使用變換減少重排和重繪。
比如給一個元素添加active 類名後它會向左移動10px,可以通過改變left 屬性:
.box{position:relative;left:0;}.active{left:-10px;}
也可以用能夠達到相同效果但性能更好的translate:
.active{transform:translateX(-10px);}
透明度
可以通過改變opacity 的值,實現元素的顯示和隱藏(與改變display 或者visibility 的值達到類似的效果類似,但性能更好)。比如實現菜單的切換效果:菜單展開時,opacity 值為1;收起時,opacity 值變為0。要注意的是pointer-events 的值也要隨之改變,防止用戶操作到明明收起的菜單。 closed 類名會根據用戶點擊'open' 時,closed 類名會被加上;點擊'close' 按鈕時,closed 類名會被移除。對應的代碼是這樣的:
.menu{opacity:1;transition:.2s;}.menu.closed{opacity:0;pointer-events:none;}
另外,透明度可變意味著開發者可以控制元素的可見程度。多多思考應用透明度的場景-- 比如直接給元素的陰影(box-shadow) 做動效很可能會造成嚴重的性能問題:
.box{box-shadow:1px1px1pxrgba(0,0,0,.5);transition:.2s;}.active{box-shadow:1px1px1pxrgba(0,0,0,1);}
如果把陰影放到偽元素上,控制偽元素的透明度從而控制陰影,效果一樣但性能更好,代碼如下:
.box{position:relative;}.box:before{content:“”;box-shadow:1px1px1pxrgb(0,0,0);opacity:.5;transition:.2s;position:absolute;width:100%;height:100%;}.box.active:before{opacity:1;}
手動優化
還有一個好消息— 開發者可以選擇想要控制的屬性,創建複合層,並將元素拖到該層。通過手動優化,確保元素總能被繪製好,這也是通知瀏覽器準備繪製該元素的最簡單方式。需要獨立層的場景包括:元素的狀態將發生一些變化(比如動畫)、改變了很消耗性能的樣式(比如position:fixed 和overflow:scroll)。可能你也見過了糟糕的性能導致了頁面閃爍、震動…或其他不如預期的效果,例如移動端常見的固定在視口頂部的頭部,會在頁面滾動的時候閃爍。將這樣的元素獨立到自己的複合層,就是常見的解決這類問題的方法。
hack方法
從前,開發者通常是通過backface-visibility:hidden 或者trasform: translate3d(0,0,0) 觸發瀏覽器生成新的複合層,但這並不是標準的寫法,這兩種寫法也對元素的視覺效果不起作用。
新方法
現在有了will-change ,它能夠顯式地通知瀏覽器對某一個元素的某個或某些元素做渲染優化。 will-change 接收各種各樣的屬性值,比如一個或多個CSS 屬性(transform, opacity)、contents或者scroll-position。不過最常用值可能就是auto,這個值表示的是瀏覽器將進行默認的優化:
.box{will-change:auto;}
優化有度,我們總能聽到關於「複合層過多反而阻礙渲染」的討論。因為瀏覽器已經為優化做了能做的一切, will-change 的性能優化方案本身對資源要求很高。如果瀏覽器持續在執行某個元素的will-change ,就意味著瀏覽器要持續對這個元素的進行優化,性能消耗造成頁面卡頓。過多的複合層降低頁面性能的現像在移動端很常見。
動畫方法
想要元素動起來可以用CSS(聲明式),也可以使用JavaScript(命令式),按需選擇。
聲明式動畫
CSS 動畫是聲明式的(告訴瀏覽器要做什麼),瀏覽器需要知道動畫的起始狀態和終止狀態,這樣它才知道如何優化。 CSS 動畫不是在主線程中執行,不會妨礙主線程中的任務執行。總的來說, CSS動畫對性能更友好。關鍵幀的動畫組合提供了相當豐富的視覺效果,比如下面是一個元素的無限旋轉動畫:
@keyframesspin{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}.box{animation-name:spin;animation-duration:3ms;animation-iteration-count:infinite;animation-timing-function:linear;}
但CSS 動畫缺乏JS 的表達能力,將兩者結合起來效果更好:比如用JS 監聽用戶輸入,根據動作切換類名。類名對應著不同的動畫效果。下面的代碼實現的是當元素被點擊時切換類名:
constbox=document.getElementById("box")box.addEventListener("click",function(){box.classList.toggle("class-name");});
值得一提的是,如果你在操作「出血」(注:設計中在畫布四邊留出的一定區域稱為「出血」)時,新的Web Animation API會利用CSS的性能。通過這個API,開發者能輕鬆地在性能友好的基礎上處理動畫的同步和時間問題。
命令式動畫
命令式動畫告訴瀏覽器如何去演繹動畫。 CSS 動畫代碼在某些場景下會變得很臃腫,或者需要更多的交互控制,此時JS 就要介入了。注意!和CSS 動畫不同,JS 動畫是在主線程中執行的(也就是說丟幀的可能性大於CSS 動畫的),性能相對差一些。在使用JS 動畫的場景中,考慮範圍中的性能之選比較少。
requestAnimationFrame
requestAnimationFrame對性能友好,你可以將它視作setTimeout的進化版,不過這其實是一個動畫執行的API。理論上調用了這個API 就能保證60fps 的幀率,但實踐證明這個函數是請求在下一次可用時繪製動畫,也就是並沒有固定的時間間隔。瀏覽器會把頁面上發生的變化組合接著一次繪製,而不會為每一次變化都進行繪製,通過這個方式提升CPU 的使用率。
RAF 可以遞歸地使用:
function doSomething () { requestAnimationFrame ( doSomething ); // Do stuff } doSomething ();
另外,類似縮放窗口或頁面滾動這樣的場景,直接綁定事件是相對消耗性能的,開發者可以考慮在類似情況下用RAF 提升性能。
滾動
實現性能良好的平滑滾動可是個挑戰。幸運的是,最近規範提出了一些可配置選項。開發者不再需要通過禁止瀏覽器默認行為(preventDefault),開啟Passive event listeners即可提升滾動性能(聲明之後,就不需要通過阻止元素的touch事件監聽和鼠標滾輪事件監聽來優化滾動性能)。使用方法僅是在需要的監聽器中聲明{passive: true}:
element.addEventListener('touchmove',doSomething(),{passive:true});
從Chrome 56 開始,這個選項將在touchmove 和touchstart 中默認開啟。
新出的Intersection Observer API將告訴開發者某個元素是不是在視口內,或者是不是和其他元素有交互。和通過事件處理這種會阻塞主線程的交互方式相比,Intersection Observer API 可以監聽元素,只有當元素交叉路徑的時候才會執行相應操作。這個API 在無限滾動和懶加載的場景都可以使用。
先讀後寫
不斷地讀寫DOM 會導致「強制同步佈局」(forced synchronous layouts),不過在技術發展過程中它演變成了更形象的詞— 「佈局抖動」(layout thrashing)。前文也有提到,瀏覽器會追踪「臟元素」,在合適的時候將變換過程儲存起來。在讀取了特定屬性以後,開發者可以強制瀏覽器提前計算。這樣反复的讀寫會導致重排。幸運的是有一個簡單的解決方式:讀完再寫。
為了模擬上述效果,請看下面這個對讀寫有嚴苛要求的例子:
boxes.forEach(box=>{box.style.transform="translateY("+wrapper.getBoundingClientRect().height+"px)";})
將「讀」放到forEach 外面,而不是和「寫」一起在每個迭代裡都執行,就能提高性能:
letwrapperHeight=wrapper.getBoundingClientRect().height+'px';boxes.forEach(box=>{box.style.transform="translateY("+wrapperHeight+"px)";})
優化的未來
瀏覽器在性能優化方面持續投入了越來越多的精力。通過新屬性contain可以聲明一個元素的子樹獨立於頁面的其他元素(目前只有Chrome和Opera支持該屬性)。這就等於告訴了瀏覽器「這個元素是安全的,它不會影響到其他元素」。 contain 的屬性值根據變化的範圍確定,可以是strict、content、size、layout、style 或者paint。這確保了子樹被更新的時候,不會造成父元素的重排(譯者稍微研究了這個屬性,有興趣的同學可以戳這裡)。特別是在引入第三方控件的時候:
.box{contain:style;//限制样式范围在元素和它的子元素中}
性能測試
知道瞭如何優化頁面性能後,還要做性能測試才行。依我之見,Chrome 開發者工具就是最棒的測試工具。在'More Tools' 中有一個'Rendering' 面板,其中包含了一些選項:比如追踪「臟元素」、計算每秒的幀率、高亮每層的邊界還有監測滾動性能問題。 (譯者註:更多關於Timeline工具的使用和解釋,請訪問這篇Chrome團隊的文章)

<'Rendering'面板中的可選項>
'Performance' 面板中的'Timeline' 工具能記錄動畫過程,開發者可以直接定位到出問題的部分。很簡單,紅色表示有問題,綠色表示渲染正常。開發者可以直接點擊紅色區域,看看是哪個函數造成了性能問題的函數。
另一個有趣的工具是在'Caputrue Settings' 中的'CPU throtting',開發者可以通過這個選項模擬頁面運行在一台非常卡的設備上。開發者在桌面瀏覽器上測試頁面的時候效果可能很好,那是因為PC 或者Mac 的本身性能就優於移動設備。這個選項提供了很好的真機模擬。

<一條合格的'Timeline'>
測試和迭代
動畫性能優化最簡單的方案就是減少每一幀的工作量。最有效緩解性能壓力的方法就是,盡量只更新在復合層中的元素,重新渲染複合層元素不容易影響到頁面上其他元素。性能優化往往意味著反复地測試和驗證,以及跳出慣性思維找到奇技淫巧實現高性能動畫— 無論怎麼樣,最終受益的會是用戶和開發者。
