實現達到 60FPS 的高性能交互動畫

q416014035 7年前發布 | 29K 次閱讀 高性能 前端技術

高性能的 Web 交互動畫:如何達到 60FPS

每一個追求自然效果的產品都希望擁有一套順暢的交互流程。但開發者可能會忽略一些細節,導致出現性能糟糕的 Web 動畫,不僅會產生“頁面垃圾”(janky),最直接的體驗就是頁面卡頓。開發者往往會花大量精力在優化首屏加載,為了幾毫秒錙銖必較,但忽略了頁面交互動畫所帶來的性能問題。

Algolia 的每一位同事都很關注用戶體驗,「性能」一定是這個話題里無法回避的關鍵部分。動畫性能之于頁面的重要性,就像搜索結果速度之于搜索一樣。

成功的標準

動畫幀率可以作為衡量標準,一般來說畫面在 60fps 的幀率下效果比較好。換算一下就是,每一幀要在 16.7ms (16.7 = 60/1000) 內完成渲染。因此,我們的首要任務是減少不必要的性能消耗。 越多的幀需要渲染的,意味著有越多的任務需要瀏覽器處理,所以掉幀就出現了,這是達到 60fps 的一個絆腳石。如果所有動畫都無法在 16.7ms 渲染完畢,不如考慮用略低的 30fps 幀率來渲染。

瀏覽器 101:像素是怎么來的

在深入研究之前,我們要先搞清楚一個很重要的問題:瀏覽器是怎么把代碼轉化成為用戶可見的像素點呢?

首次加載時,瀏覽器會下載并解析 HTML,將 HTML 元素轉變為一個 DOM 節點的「內容樹」(content tree)。除此之外,樣式同樣會被解析生成「渲染樹」 (render tree)。為了提升性能,渲染引擎會分開完成這些工作,甚至會出現渲染樹比 DOM 樹更快生成出來。

布局

渲染樹生成后,瀏覽器會從頁面左上角開始迭代地計算出每個元素尺寸和位置,最終生成布局。這個過程可能是一氣呵成的,但也可能由于元素的排列導致反復地繪制。元素間的位置關系都緊密相關。為了優化必要的任務,瀏覽器會追蹤元素的變化情況,并將這些元素以及它們的子節點標記為 ‘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; }</code></pre>

另外,透明度可變意味著開發者可以控制元素的可見程度。多多思考應用透明度的場景 -- 比如直接給元素的陰影 ( box-shadow ) 做動效很可能會造成嚴重的性能問題:

.box {
    box-shadow: 1px 1px 1px rgba(0,0,0,.5);
    transition: .2s;
}
.active {
   box-shadow: 1px 1px 1px rgba(0,0,0,1);
}

如果把陰影放到偽元素上,控制偽元素的透明度從而控制陰影,效果一樣但性能更好,代碼如下:

.box {
   position: relative;
}
.box:before {
   content: “”;
   box-shadow: 1px 1px 1px rgb(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 動畫對性能更友好 。關鍵幀的動畫組合提供了相當豐富的視覺效果,比如下面是一個元素的無限旋轉動畫:

@keyframes spin {
    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 監聽用戶輸入,根據動作切換類名。類名對應著不同的動畫效果。下面的代碼實現的是當元素被點擊時切換類名:

const box = 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 外面,而不是和「寫」一起在每個迭代里都執行,就能提高性能:

let wrapperHeight = 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' 面板,其中包含了一些選項:比如追蹤「臟元素」、計算每秒的幀率、高亮每層的邊界還有監測滾動性能問題。

'Performance' 面板中的 'Timeline' 工具能記錄動畫過程,開發者可以直接定位到出問題的部分。很簡單,紅色表示有問題,綠色表示渲染正常。開發者可以直接點擊紅色區域,看看是哪個函數造成了性能問題的函數。

另一個有趣的工具是在 'Caputrue Settings' 中的 'CPU throtting',開發者可以通過這個選項模擬頁面運行在一臺非常卡的設備上。開發者在桌面瀏覽器上測試頁面的時候效果可能很好,那是因為 PC 或者 Mac 的本身性能就優于移動設備。這個選項提供了很好的真機模擬。

測試和迭代

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

 

來自:https://qianduan.guru/posts/59ccdbe44a17f976177e4fc7

 

 本文由用戶 q416014035 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!