前端產(chǎn)品直接影響用戶的體驗,而用戶體驗上,操作的流暢性對其有著至關(guān)重要的影響。也許在PC上我們還感覺不出來強烈的卡頓,但是隨著前端移動端的發(fā)展,Web 性能問題在移動端得到了急劇放大。什么才是真正的流暢呢?目前來說,與設備刷新頻率保持一致,即 60fps 才可以說是真正的操作很流暢。這也是為什么大部分移動端 App 產(chǎn)品會選擇使用原生的開發(fā)語言而非 H5 開發(fā)的一個重要原因。要知道,相對于 H5 開發(fā),原生開發(fā)的代價高、開發(fā)效率低,而且很難做到App出了問題立馬就能更新到用戶端。所以,我們需要對我們的前端產(chǎn)品進行優(yōu)化,盡量達到 60fps 的幀率,也就是說每一幀的運行時間最好不要超過 16ms。
前端性能優(yōu)化是一個很大的話題,雅虎前端優(yōu)化軍規(guī)從宏觀上告訴我們有哪些點是可以優(yōu)化的,本文主要從其中的一個點,即瀏覽器端渲染這個微觀角度上來介紹,更具體點來說,本文將結(jié)合 Chrome 調(diào)試工具來介紹常見的可優(yōu)化點。
瀏覽器渲染基本流程
先大致地了解瀏覽器端渲染一個頁面的基本流程。
當瀏覽器接收到服務器端的頁面內(nèi)容之后,它需要對整個 HTML 結(jié)構(gòu)進行解析,形成 DOM 樹;與此同時,它還需要對相應的 CSS 文件進行解析,形成 CSS 樹(CSSOM)。接下來,需要結(jié)合 DOM + CSSOM,形成一個繪制樹(Render Tree)。Render 樹與 DOM 很像,區(qū)別在于,它不會包含 DOM 中的 head 等與渲染內(nèi)容無關(guān)的結(jié)點,也不會包含 CSS 中定義為不可見的結(jié)點(對于 CSS 中定義的偽元素,如果可見,也會出現(xiàn)在繪制樹中)。
得到繪制樹之后,需要計算每個結(jié)點在頁面中的位置,這一個過程稱為layout(也有的稱為reflow)。值得注意的是,layout 是一個計算代價相對很大的過程,它需要從根節(jié)點進行遍歷,對每個可出現(xiàn)在頁面中的節(jié)點進行位置的計算。
由于layout的過程中,我們是在一個連續(xù)的二維平面上進行的,接下來,需要將這些結(jié)果柵格化,映射到屏幕的離散二維平面上,這一過程稱為 paint。paint實際上包含了兩個任務:繪制(調(diào)用底層類似于Canvas的繪制API) + 柵格化(由composite線程控制,將繪制結(jié)果上傳到GPU用于組合拼裝頁面)。Chrome調(diào)試窗口中,實心綠色框表示繪制,空心綠色框表示的是柵格化。現(xiàn)代瀏覽器為提升性能,將頁面劃分多個 layer,各自進行 paint 然后組合成一個頁面(composite layers),可以與 PhotoShop 中的圖層概念進行類比。這樣做有什么好處呢?這樣我們可以充分地利用 GPU 的并行處理能力,將某些 layer 傳送到 GPU中進行繪制,即一系列的像素集合,并將結(jié)果傳送到 CPU 中組裝成一個頁面。
這樣我們得到的就是首次頁面繪制的過程了。接下來,每一幀的過程中,又會發(fā)生什么事情呢?如下:
JavaScript => Style => Layout => Paint => Composite
每一幀中,如果 JS 動態(tài)地修改了 DOM 或 CSSOM,就會引起樣式的更新,從而引起頁面的 re-Layout,然后重新繪制結(jié)果并重新組裝 layer。需要說明的是,并不是所有的修改都會引起上述所有的更新。如我們修改背景顏色的時候,就不會引起 Layout 的更新;但我們?nèi)绻薷?width 等布局屬性,則會引起上面的變化;而如果修改 transform: translate3d 這種硬件加速屬性,Layout和Paint都不會更新,這會大大加速頁面的性能。關(guān)于什么屬性引起哪些更新,可以參考 CSS TRIGGERS。
這也說明,在使用JavaScript進行動畫的過程中,我們應該盡早在每一幀的開始執(zhí)行 JS,這也是為什么推薦使用 requestAnimationFrame 的原因,因為它在每一幀刷新的時候都會調(diào)用,而且確保是每一幀最開始執(zhí)行。
從另一個角度來看,頁面生存周期內(nèi)的狀態(tài)及其用戶可接受時間可以大致分為:加載數(shù)據(jù)(<1000ms)、響應(<100ms)、空閑期(50ms)、動畫期(<16ms)。顯然,我們的優(yōu)化重點應該是在動畫期,對于加載響應數(shù)據(jù)的優(yōu)化,可以參考雅虎優(yōu)化準則。接下來,我們就把我們的重點放在動畫期的性能優(yōu)化上,利用Chrome調(diào)試工具找出那些引起頁面卡頓(Jank)的罪魁禍首。
性能優(yōu)化
使用 Timeline 視圖,我們可以清晰地看到每一幀執(zhí)行的時間,具體發(fā)生了哪些事情,有哪些引起單幀時間過長。關(guān)于具體如何使用Timeline,這里不做介紹,畢竟它是一個實踐性的東西。這里列舉一下一些常見的性能問題:
- JavaScript 代碼中出現(xiàn)的計算密集型任務,引起單幀時間太長。例如圖像處理、對萬級別的數(shù)據(jù)進行冒泡排序,顯然它會導致頁面直接卡頓,體驗很差。這種情況下,一方面我們需要優(yōu)化算法,另一方面,可以考慮使用 WebWorker 在另一個線程中進行計算??梢詤⒖迹?a target="_blank" rel="nofollow">http://www.html5rocks.com/en/tutorials/workers/basics/
- CSS樣式的改變(不管是JS代碼中改變的還是通過CSS3動畫改變的):
- CSS選擇器:大量使用偽類的選擇器性能會有所下降(示例:http://jsbin.com/gozula/1/quiet),這種情況下考慮選擇器直接作用在子元素上,而非通過偽類來選擇,推薦使用BEM形式的選擇器。另外,直接設 className 性能不如
classList設置。classList.remove(‘XXX’); classList.add(‘XXX’); - 某些CSS屬性的更新會引起大量的 reflow,從而引起單幀時間太長。這種情況下,我們應該盡量減少或避免在動畫過程中引起reflow。頻繁地讀取 offsetWidth, getComputedBoundingRect 等方法很容易引起 reflow 的情況。
- 對引起reflow的CSS屬性進行讀寫分離,避免反復的
JS->style->layout->JS對頁面進行多次reflow。經(jīng)常看到Timeline中提示Forced Synchronous Layout就屬于這種情況,這些屬性可以參考 CSS TRIGGERS 網(wǎng)站。http://output.jsbin.com/cahapijazu/1 VS. http://output.jsbin.com/cahapijazu/2 - CSS引起頁面的大量 repaint ,這種情況很大程度上頁面上大量的不必要內(nèi)容被重新繪制了。當頁面全堆積到了一個層(Layer)上,就會出現(xiàn)這種情況,will-change可用于提醒瀏覽器示例:http://www.html5rocks.com/static/demos/parallax/demo-1a/demo.html (優(yōu)化后的結(jié)果:https://dl.dropboxusercontent.com/u/2272348/codez/parallax/demo-promo/index.html,可以把#background設置will-change屬性以避免滾動時整個#background的大量repaint)和 http://output.jsbin.com/raruwi/1/quiet。另外,大量的圖片 resize 操作也會引起 repaint。
// 反復讀寫的例子,這種情況下我們應該先把讀操作緩存起來
for (var i = 0; i < paras.length; i++) {
var width = block.offsetWidth;
paras[i].style.width = width + ‘px’;
}
推薦課程: