上篇文章《網站性能優化——CRP》已經介紹過網站性能優化中的關鍵渲染路徑部分,至關於從一個「宏觀」的角度去優化性能,固然,這個角度也是最重要的優化。本篇就從一個「微觀」的層面去優化——瀏覽器渲染。javascript
在視頻領域,電影、電視、數字視頻等可視爲隨時間連續變換的許多張畫面,而幀則指這些畫面當中的每一張。——維基百科css
網頁上來講,其實就是指瀏覽器渲染出的頁面。目前大多數設備的屏幕刷新頻率爲60次/秒(60fps),每一幀所消耗的時間約爲16ms(1000 ms / 60 = 16.66ms),但實際上,瀏覽器還有一些整理工做要作,所以開發者所作的全部工做須要在10ms內完成。 前端
若是不能完成,幀率將會降低,網頁會在屏幕上抖動,也就是一般所說的卡頓,這會對用戶體驗產生嚴重的負面影響。因此若是一個頁面中有動畫效果或者用戶正在滾動頁面,那麼瀏覽器渲染動畫或頁面的速率也要儘量地與設備屏幕的刷新頻率保持一致,以保證良好的用戶體驗。java
提升幀率,其實就是優化瀏覽器渲染頁面的過程。當你在工做時,須要瞭解並注意五個主要的區域,這些區域是你能在最大程度上去控制的地方,固然,也就是優化性能、提升幀率的地方。git
JavaScript:通常狀況下,咱們會使用JS去處理一些致使視覺變化的工做,好比動畫或者增長DOM元素等。固然,除了JS,還有其餘一些方法,好比:CSS Animations、Transitions、 Web Animation APIgithub
Style calculations:這個過程是根據匹配選擇器(.nav > .nav-item)計算出哪些CSS規則應用在哪些元素上面的過程web
Layout:瀏覽器知道對一個元素應用哪些規則以後,就能夠開始計算這個元素佔據的空間大小及其在屏幕上的位置segmentfault
Paint:繪製是填充像素的過程。它涉及繪出文本、顏色、圖像、邊框和陰影,基本上包含了元素的每一個可視部分。繪製通常是在多個層上完成的瀏覽器
Compositing(合成):因爲頁面的不一樣部分可能被繪製到多個層上,所以它們須要按照正確的順序繪製到屏幕上以正確渲染頁面性能優化
像素管道的每一個部分都有可能產生卡頓,所以,準確瞭解你的代碼會觸發管道的哪些部分十分重要。
幀不必定都會通過管道每一個部分的處理。實際上,在改變視覺呈現時,針對指定幀,管道的運行一般有三種方式:
JS / CSS > Style > Layout > Paint > Composite當改變了某個元素的幾何屬性(如width、height,或者表示位置的left、top等)——即修改了該元素的「佈局(layout)」屬性,那麼瀏覽器將會檢查全部其餘元素,而後對頁面進行「重排(reflow)」。任何受到影響的區域都須要從新繪製,而後進行合成。
JS / CSS > Style > Paint > Composite當改變了只與繪製相關的屬性(如背景圖片、文字顏色或陰影等),即不會影響頁面的佈局,則瀏覽器會跳過佈局階段,但仍須要執行繪製、合成。
JS / CSS > Style > Composite當改變了一個既不須要「重排」也不須要「重繪」的屬性(如transform),則瀏覽器將跳過佈局、繪製階段,直接執行合成。
requestAnimationFrame
應該做爲開發者在建立動畫時的必備工具,它會確保JS儘早在每一幀的開始執行。
以前咱們可能看到過不少用setTimeout
和setInterval
建立的動畫,好比老版本的jQuery。可是使用這兩個函數建立的動畫效果可能不夠流暢,JS引擎在安排這兩個函數時根本不會關注渲染通道,參考《Html5 Canvas核心技術》中的論述:
1.即便向其傳遞毫秒爲單位的參數,它們也不能達到ms的準確性。這是由於javascript是單線程的,可能會發生阻塞。
2.沒有對調用動畫的循環機制進行優化。
3.沒有考慮到繪製動畫的最佳時機,只是一味地以某個大體的事件間隔來調用循環。
前面討論過刷新一幀消耗的最佳時間大概在10ms左右,可是一幀裏面一般又包括JS處理、樣式處理、佈局、渲染等等,因此JS執行的時間最好控制在3~4ms。JS在瀏覽器的主線程上運行,若是運行時間過長,就會阻塞樣式計算、佈局等工做,這樣可能致使幀丟失。
許多狀況下,能夠將純計算性的工做移到Web Worker,好比,不須要訪問DOM的時候。數據操做或者遍歷(如排序或搜索)每每很適合這種模型,加載和模型生成也是如此。
當覺察到頁面有卡頓的時候但又不知道是哪部分的JS形成的,這時能夠打開Timeline錄製時間軸,查看、分析是哪一個地方的JS形成了頁面卡頓,而後作針對性的JS優化。有關Timeline的使用,請參考《Chrome DevTools - Timeline》。
計算樣式(computing styles)的第一部分是建立一組匹配選擇器,以便瀏覽器計算出給指定元素應用哪些類、僞選擇器和 ID。第二部分涉及從匹配選擇器中獲取全部樣式規則,並計算出此元素的最終樣式。
在當前的Chrome渲染引擎中,用於計算某元素計算樣式的時間中大約有 50% 用來匹配選擇器,而另外一半時間則用於從匹配的規則中構建 RenderStyle。
下降選擇器的複雜度:能寫出高效率選擇器的前端開發者原本就很少,又加上當前Less和Sass的普及,一些前端開發者對Less、Sass的濫用,致使編譯後的css選擇器有時候甚至能達到六七層嵌套,這大大增長了瀏覽器計算樣式所消耗的總時間。
最理想的狀態是每一個元素都有一個惟一的id,這樣選擇器最簡單也是最高效的,但是咱們知道這是不現實的。可是,遵循一些指導原則依然能讓咱們寫出較爲高效的CSS選擇器:Writing efficient CSS selectors
在修改CSS樣式時,內心要清楚哪些屬性會觸發佈局操做,能避免則避免。考慮到實際的開發狀況,幾乎避免不了啊~~若是沒法避免,則要使用Timeline查看一下佈局要花多長時間,並肯定佈局是否會形成性能瓶頸。若是佈局消耗時間過多,則要從佈局前面的JS和樣式階段查找一下緣由,並作進一步的優化。
想知道哪些CSS屬性會觸發佈局、繪製或合成?請查看CSS觸發器
若是用定位、浮動和flexbox都能達到相同的佈局效果,在瀏覽器兼容的狀況下,優先使用flexbox佈局,不只由於其功能強大,更是由於其性能在佈局上更勝一籌。
將一幀繪製到屏幕上會經歷如下順序:
首先執行JS,而後計算樣式,而後佈局。可是,某些JS有可能強制瀏覽器提早執行佈局操做,變成 JS > Layout > Styles > Layout > Paint > Composite,這被稱爲強制同步佈局(Forced Synchronous Layout)。
用一個demo來講明一下FSL:
點擊Trigger按鈕,改變上面三個按鈕的寬度,index.js內容以下:
1. var element1 = document.querySelector('.btn1'); 2. var element2 = document.querySelector('.btn2'); 3. var element3 = document.querySelector('.btn3'); 4. var triggerBtn = document.querySelector('.trigger'); 5. triggerBtn.addEventListener('click', function trigger(){ 6. // Read 7. var h1 = element1.offsetWidth; 8. // Write (invalidates layout) 9. element1.style.width = (h1 * 2) + 'px'; 10. 11. // Read (triggers layout) 12. var h2 = element2.offsetWidth; 13. // Write (invalidates layout) 14. element2.style.width = (h2 * 2) + 'px'; 15. 16. // Read (triggers layout) 17. var h3 = element3.offsetWidth; 18. // Write (invalidates layout) 19. element3.style.width = (h3 * 2) + 'px'; 20. });
能夠看到,讀取offsetWidth
屬性會致使layout。可是,要注意的是,在 JS 運行時,來自上一幀的全部舊佈局相關的值是已知的,而且可供查詢。因此,在Timeline中看到第7行代碼只是觸發了Recalculate Style事件,並未觸發Layout事件。當JS執行到第12行代碼的時候,爲了獲取element2.offsetWidth
,瀏覽器必須先執行計算樣式(由於第9行代碼改變了element1的width屬性),而後執行佈局,才能返回正確的寬度,第17行代碼也是如此。這是沒必要要的,並且可能致使很大的時間開銷。JS執行到第19行時,觸發最終的Recalculate Style事件和Layout事件,渲染出新的一幀。
避免強制同步佈局:先讀取佈局屬性,而後批量處理樣式更改。
... 6. // Read 7. var h1 = element1.clientHeight; 8. var h2 = element2.clientHeight; 9. var h3 = element3.clientHeight; 10. 11. // Write (invalidates layout) 12. element1.style.height = (h1 * 2) + 'px'; 13. element2.style.height = (h2 * 2) + 'px'; 14. element3.style.height = (h3 * 2) + 'px'; // Document reflows at end of frame
能夠看到,先讀取佈局屬性,而後批量處理樣式更改,只會致使最終的Layout,避免了FSL。
當在頁面上進行交互時,想知道哪些區域被從新繪製了?打開DevTools的副面板,切換到Rendering,勾選「Paint Flashing」:
交互發生後,從新繪製的區域會閃爍綠色:
繪製並不是老是繪製到內存中的單個圖像上。實際上,若是必要,瀏覽器能夠繪製到多個圖像(層)上。這種方法的優勢是,按期重繪的元素,或者經過動畫變形在屏幕上移動的元素,能夠在不影響其餘元素的狀況下進行處理。這和圖像處理軟件Photoshop、Sketch等層的概念是相似的,各個層能夠在彼此的上面處理併合成,以建立最終圖像。
建立新層的最佳方式是使用will-change
CSS 屬性,當其屬性值爲transform
時,將會建立一個新的合成器層(compositor layer):
.moving-element { will-change: transform; }
對於不支持will-change
屬性的瀏覽器,可使用如下css作兼容處理:
.moving-element { transform: translateZ(0); }
須要注意的是:不要建立太多層,由於每層都須要內存和管理開銷。若是你已將一個元素提高到一個新層,最好使用 DevTools 確認一下這樣作能帶來性能優點。請勿在不分析的狀況下提高元素。
最後說一下如何使用Timeline瞭解網頁中的層。
勾選Paint,而後錄製Timeline,而後點擊單個幀,這時詳情選項裏面多了個「layer」選項卡,切換到此選項卡。展開左側#document
,便可看到頁面裏面有多少個層(layer),單擊每一個層時,右側還會顯示這個層被建立的緣由。
若是在性能關鍵操做期間(好比滾動或動畫)花了不少時間在合成上(應當力爭在4-5ms左右),則可使用此處的信息來查看頁面有多少層、建立層的緣由,進一步去管理頁面中的層數。
References