該文章於三天前發表在 github,如有問題可提至 github.css
這篇文章主要關注的是資源加載以後的性能,由於大多數用戶關注的不是應用如何加載而是具體的使用。因此要快速響應用戶,尤爲是無線端,咱們有必要了解瀏覽器渲染性能。git
首先一個須要思考的問題,怎樣的網站是順暢的?咱們可能能夠給一個大概的感受,如:秒級響應等。其實,也能夠給出一個很討巧的答案:用戶以爲順暢的網站它就是順暢的。由於幾乎全部網站都但願將用戶留在頁面上,固然以用戶爲中心創建性能模型是必要的。下面是 Google 提出的一個以用戶爲中心的性能模型,裏面的數據不是 Google 獨創,有一些論文作相似研究(如:100ms 響應用戶是一個很合適的時間等)。github
上圖是 RAIL 的具體含義,這裏有一些關鍵性的數據指標:web
Respond:0 - 100ms,視窗通常須要在這個時間段響應用戶,超過這個時間段,用戶就會感受到延時。chrome
Animation:0~16ms,屏幕每秒刷新60次,16ms 表明的是每一幀的時間。用戶是很是關注動畫的,當動畫失幀很容易引發用戶察覺。因此動畫通常要控制在60FPS。瀏覽器
Idle:最大化主進程的空閒時間,這樣能夠及時響應用戶輸入。安全
Load:內容須要在1000ms 內加載出來,超過1000ms 會以爲加載緩慢。性能優化
應用要達到上面的性能模型須要從哪些方面入手呢?若是咱們知道瀏覽器是如何渲染一個頁面的,而且去優化渲染過程當中的關鍵步驟,是否是就能事半功倍呢?網絡
上圖是瀏覽器渲染的關鍵路徑,首先,讓咱們從瀏覽器解析一個頁面開始吧。異步
轉化: 瀏覽器從磁盤或網絡讀取 HTML 的原始字節,瀏覽器會將這段原始文件按照相應編碼規範進行解碼(如今通常爲 utf-8)。
符號化:根據 W3C 標準轉化爲對應的符號(通常在尖括號內)。
DOM 構建:HTML 解析器會解析其中的 tag 標籤,生成 token ,遇到 CSS 或 JS 會發送相應請求。HTML 解析時阻塞主進程的,CSS 通常也是阻塞主進程的(媒體查詢時例外),也就是說它們在解析過程當中是沒法作出響應的。而 JS 手動添加 async 後達到異步加載,根據 token 生成相應 DOM 樹。
。
CSSDOM 構建,添加 CSS 樣式生成 CSSDOM 樹。
渲染樹構建,從 DOM 樹的根節點開始,遍歷每一個可見的節點,給每一個可見節點找到相應匹配的 CSSOM 規則,並應用這些規則,連帶其內容及計算的樣式。
樣式計算,瀏覽器會將全部的相對位置轉換成絕對位置等一系列的樣式計算。
佈局,瀏覽器將元素進行定位、佈局。
繪製,繪製元素樣式,顏色、背景、大小、邊框等。
合成,將各層合成到一塊兒、顯示在屏幕上。
若是咱們是作一個動畫,通常會用 JS 更改相應樣式,接着瀏覽器就會經歷 JS 運行、樣式計算、佈局、繪製、合成等多個重要步驟(後面還會講到這個步驟實際過程當中能夠更長或者更短)。那麼要作的優化就是在這幾個步驟中進行優化而且儘可能去掉中間的耗時步驟。
上圖描述的四個場景都是有可能對響應用戶輸入或者動畫形成影響的。函數的輸入事件處理、不合時機的 JS 、長時間的 JS 運行以及垃圾回收。
首先,咱們要知道的一個事實就是瀏覽器是由多個處理進程的:Compositor、Tile Worker、Main。當用戶進行輸入操做(滾動、點擊等),如滾動時,Compositor 進程會接收到這個事件(實際它能夠接受任何用戶輸入事件),若是能夠的話,它將不會通知主進程,直接說:滾吧,牛寶寶。因而,頁面就滾動了。固然,這其中包含更新層定位以及讓 GPU 繪製幀,而主線程處於空閒狀態。可是,事情每每並不是如此。若是輸入事件上綁定了 JS 處理事件的話,Compositor 進程就沒辦法主動跳過主進程了。
如上圖,當 JS 處理事件過長時,輸入事件的響應會一直處於阻塞狀態,直到 JS 處理完成。當響應超過 100ms 時,用戶就會感覺到延時。因此當處理用戶事件時,咱們應該作到:
避免長時間的 JS 執行。
避免在處理中改變樣式。由於樣式改變會引發後面佈局、繪製、合成等操做。
對用戶輸入進行消抖。
其餘優化:
使用 requestAnimationFrame,將 setTimeout 換成 requestAnimationFrame,由於 setTimeout 時間控制可能形成在一幀的中間,目前各瀏覽器對 requestAnimationFrame 的支持已經比較好了。
使用 Web Workers,將複雜計算的 JS 採用 Web Workers 進行處理。
減小垃圾回收,垃圾回收是一個容易被忽略的問題,由於垃圾回收的時間是不受控制的,它可能在一個動畫的中途,阻塞動畫的執行,更理想的狀況是在循環中複用對象。
添加或移除一個 DOM 元素、修改元素屬性和樣式類、應用動畫效果等操做,都會引發 DOM 結構的改變,從而致使瀏覽器須要從新計算每一個元素的樣式、對頁面或其一部分從新佈局(多數狀況下)。
計算樣式的第一步是建立一套匹配的樣式選擇器,瀏覽器就是靠它們來對一個元素應用樣式的。第二步是根據匹配的樣式選擇器來獲取對應的具體樣式規則,計算出最終具體有哪些樣式是要應用在 DOM 元素上的。因此樣式的優化也是這兩步:
如何減少選擇器的複雜性?
.box:nth-last-child(-n+1) .title { /* styles */ } .final-box-title { /* styles */ }
上面代碼都是選擇同一個元素,當元素不少時,第二個選擇器的性能會明顯優於第一個。BEM 規範有作相似事情,按照特性直接由一個選擇器選擇元素的性能每每會更優。
由於元素的計算量和被改變的元素的數量成正比,因此你只須要注意一點,減小無效元素。
<div> <div> <p>多層無心義的標籤</p> </div> </div>
像上面的例子,有時候建立了一些冗餘的標籤。當改變外層的樣式時,冗餘的標籤也須要進行樣式計算,浪費性能。
瀏覽器計算 DOM 元素的幾何信息的過程:元素大小和在頁面中的位置。每一個元素都有一個顯式或隱式的大小信息,決定於其 CSS 屬性的設置、或是元素自己內容的大小、或者是其父元素的大小。在 Blink/WebKit 內核的瀏覽器和 IE 中,這個過程稱爲 Layout。在基於 Gecko 的瀏覽器(好比 Firefox)中,這個過程稱爲 Reflow。
目前,transform 和 opacity 只會引發合成,不會引發佈局和從新繪製。整個流程中比較耗費性能的佈局和繪製流程將直接跳過,性能顯然是很好的。其餘的 CSS 屬性改變引發的流程會有所不一樣,有些屬性也會跳過佈局,具體能夠查看 CSS Triggers。因此,優化的第一步就是儘量避免觸發佈局。
Flexbox 佈局方案性能會優於之前的佈局方案,並且目前瀏覽器對 Flexbox 支持度至關高了:
首先是執行 JS 腳本,而後是樣式計算,而後是佈局。可是,咱們還能夠強制瀏覽器在執行 JS 腳本以前先執行佈局過程,這就是所謂的強制同步佈局。在 JS 腳本運行的時候,它能獲取到的元素樣式屬性值都是上一幀畫面的,都是舊的值。所以,若是你想在這一幀開始的時候,讀取一個元素的 height 屬性,你能夠會寫出這樣的 JS 代碼:
function logBoxHeight() { box.classList.add('super-big'); // Gets the height of the box in pixels // and logs it out. console.log(box.offsetHeight); }
爲了給你返回 box 的 height 屬性值,瀏覽器必須首先應用 box 的屬性修改(由於對其添加了 super-big 樣式),接着執行佈局過程。在這以後,瀏覽器才能返回正確的 height 屬性值。這樣就形成了同步佈局事件,是很是消耗性能的。大多數狀況下,你應該都不須要先修改而後再讀取元素的樣式屬性值,使用上一幀的值就足夠了。過早地同步執行樣式計算和佈局是潛在的頁面性能的瓶頸之一。
function logBoxHeight() { // Gets the height of the box in pixels // and logs it out. console.log(box.offsetHeight); box.classList.add('super-big'); }
還有一種狀況比強制同步佈局更糟:連續快速的屢次執行它。
function resizeAllParagraphsToMatchBlockWidth() { // Puts the browser into a read-write-read-write cycle. for (var i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = box.offsetWidth + 'px'; } }
上述代碼對一組段落標籤執行循環操做,設置 p 標籤的width屬性值,使其與 box 元素的寬度相同。看上去這段代碼是沒問題的,但問題在於,在每次循環中,都讀取了 box 元素的一個樣式屬性值,而後當即使用該值來更新 p 元素的 widt h屬性。在下一次循環中讀取 box 元素 offsetwidth 屬性的時候,瀏覽器必須先使得上一次循環中的樣式更新操做生效,也就是執行佈局過程,而後才能響應本次循環中的樣式讀取操做。佈局過程將在每次循環中發生。優化代碼:
// Read. var width = box.offsetWidth; function resizeAllParagraphsToMatchBlockWidth() { for (var i = 0; i < paragraphs.length; i++) { // Now write. paragraphs[i].style.width = width + 'px'; } }
若是你想確保編寫的讀寫操做是安全的,你可使用 FastDOM。它能幫你自動完成讀寫操做的批處理,還能避免意外地觸發強制同步佈局或快速連續的佈局。
繪製並不是老是在內存中的單層畫面裏完成的。實際上,瀏覽器在必要時將會把一幀畫面繪製成多層畫面,而後將這若干層畫面合併成一張圖片顯示到屏幕上。經過渲染層提高能夠減少繪製區域,咱們能夠用調試工具查看到繪製層:
在頁面中新建一個渲染層最好的方式就是使用 will-change 屬性,同時再與 transform 屬性一塊兒使用,就會建立一個新的組合層:
.element { will-change: transform; }
對於那些目前還不支持 will-change 屬性、但支持建立渲染層的瀏覽器,可使用一個 3D transform 屬性來強制瀏覽器建立一個新的渲染層:
.element { transform: translateZ(0); }
注意: 別盲目建立渲染層,必定要分析其實際性能表現。由於建立渲染層是有代價的,每建立一個新的渲染層,就意味着新的內存分配和更復雜的層的管理。而且在移動端 GPU 和 CPU 的帶寬有限制,建立的渲染層過多時,合成也會消耗跟多的時間。
有時候,儘管把元素提高到了一個單獨的渲染層,瀏覽器會把兩個相鄰區域的渲染任務合併在一塊兒進行,這將致使整個屏幕區域都會被繪製。因此可使用調試工具查看,仔細規劃動畫。
不一樣的 CSS 屬性繪製的成本是不同的,繪製一個陰影就比繪製邊框更費時。固然,這個瀏覽器也在不停優化中,如今的耗時渲染屬性隨時均可能被改變,因此須要多關注一下。
渲染層的合併,就是把頁面中完成了繪製過程的部分合併成一層,而後顯示在屏幕上。下面和合成相關的兩點前面也有提到過。
前面已經提到過 transform/opacity 的優點,應用了 transforms/opacity 屬性的元素必須獨佔一個渲染層。爲了對這個元素建立一個自有的渲染層,你必須提高該元素。
建立一個新的渲染層須要消耗額外的內存和管理資源。而在內存資源有限的設備上,因爲過多的渲染層來帶的開銷而對頁面渲染性能產生的影響,甚至遠遠超過了它在性能改善上帶來的好處。因爲每一個渲染層的紋理都須要上傳到 GPU 處理,所以咱們還須要考慮 CPU 和 GPU 之間的帶寬問題、以及有多大內存供 GPU 處理這些紋理的問題。
關注趨勢,今天不少的性能瓶頸極可能在未來都再也不是問題。如以前關注的一項技術 Web Animations,是否能用 JS 達到原生動畫效果。Houdini,你能夠添加更多的 JS 代碼到動畫中而不用擔憂性能問題。
利用工具 Chrome DevTools
,上面的規則只是優化的方向,善於利用工具分析。移動端利用 inspector 也是很是方便的,而且還能夠對數據進行保存,對比分析等。幾乎一切須要的分析工具,DevTools 都有。
不要進行微優化,有時花上很短的帶來的性能提高卻很小,對於平常快速迭代的業務是不必這樣作的。
Paul Lewis 相關文章,Chrome 開發團隊,旨在幫助開發者提升他們的應用和站點。
Performance Calendar,從2009年到2015年,列出了每年值得關注的優化方案。
Performance,Google Developers 官方站點對性能優化的文章。
webkit,webkit 的博客站點,介紹了比較底層的渲染等文章。