在瀏覽器渲染過程與性能優化一文中(建議先去看一下這篇文章再來閱讀本文),咱們瞭解與認識了瀏覽器的關鍵渲染路徑以及如何優化頁面的加載速度。在本文中,咱們主要關注的是如何提升瀏覽器的渲染性能(瀏覽器進行佈局計算、繪製像素等操做)與效率。javascript
不少網頁都使用了看起來效果很是酷炫的動畫與用戶進行交互,這些動畫效果顯著提升了用戶的體驗,但若是由於性能緣由致使動畫的每秒幀數過低,反而會讓用戶體驗變得更差(若是一個酷炫的動畫效果運行起來老是常常卡頓或者看起來反應很慢,這些都會讓用戶感受糟透了)。css
一個流暢的動畫須要保持在每秒60幀,換算成毫秒瀏覽器須要在10毫秒左右完成渲染任務(每秒有1000毫秒,1000/60 約等於 16毫秒一幀,但瀏覽器還有其餘工做須要佔用時間,因此估算爲10毫秒),若是可以理解瀏覽器的渲染過程並發現性能瓶頸對其優化,可使你的項目變得具備交互性且動畫效果如飄柔般順滑。html
本文做者爲: SylvanasSun(sylvanas.sun@gmail.com).轉載請務必將本段話置於文章開頭處(保留超連接).
本文首發自SylvanasSun Blog,原文連接: sylvanassun.github.io/2017/10/08/…java
所謂像素管道其實就是瀏覽器將渲染樹繪製成像素的流程。管道的每一個區域都有可能產生卡頓,即管道中的某一區域若是發生變化,瀏覽器將會進行自動重排,而後從新繪製受影響的區域。jquery
JavaScript:該區域其實指的是實現動畫效果的方法,通常使用JavaScript
來實現動畫,例如JQuery
的animate
函數、對一個數據集進行排序或動態添加一些DOM
節點等。固然,也可使用其餘的方法來實現動畫效果,像CSS
的Animation
、Transition
和Transform
。git
Style:該區域爲樣式計算階段,瀏覽器會根據選擇器(就是CSS
選擇器,如.td
)計算出哪些節點應用哪些CSS
規則,而後計算出每一個節點的最終樣式並應用到節點上。github
Layout:該區域爲佈局計算階段,瀏覽器會在該過程當中根據節點的樣式規則來計算它要佔據的空間大小以及在屏幕中的位置。web
Paint:該區域爲繪製階段,瀏覽器會先建立繪圖調用的列表,而後填充像素。繪製階段會涉及到文本、顏色、圖像、邊框和陰影,基本上包括了每一個可視部分。繪製通常是在多個圖層(用過Photoshop
等圖片編輯軟件的童鞋必定很眼熟圖層這個詞,這裏的圖層的含義實際上是差很少的)上完成的。瀏覽器
Composite:該區域爲合成階段,瀏覽器將多個圖層按照正確順序繪製到屏幕上。性能優化
假設咱們修改了一個幾何屬性(例如寬度、高度等影響佈局的屬性),這時Layout階段受到了影響,瀏覽器必須檢查全部其餘區域的元素,而後自動重排頁面,任何受到影響的部分都須要從新繪製,而且最終繪製的元素還須要從新進行合成(簡單地說就是整個像素管道都要從新執行一遍)。
若是咱們只修改了不會影響頁面佈局的屬性,例如背景圖片、文字顏色等,那麼瀏覽器會跳過佈局階段,但仍須要從新繪製。
又或者,咱們只修改了一個不影響佈局也不影響繪製的屬性,那麼瀏覽器將跳過佈局與繪製階段,顯然這種改動是性能開銷最小的。
若是想要知道每一個CSS
屬性將會對哪一個階段產生怎樣的影響,請去CSS Triggers,該網站詳細地說明了每一個CSS
屬性會影響到哪一個階段。
咱們常用JavaScript
來實現動畫效果,然而時機不當或長時間運行的JavaScript
可能就是致使你性能降低的緣由。
避免使用setTimeout()
或者setInterval()
函數來實現動畫效果,這種作法的主要問題是回調將會在幀中的某個時間點運行,這可能會恰好在末尾(會丟失幀致使發生卡頓)。
有些第三方庫仍在使用setTimeout()&setInterval()
函數來實現動畫效果,這會產生不少沒必要要的性能降低,例如老版本的JQuery
,若是你使用的是JQuery3
,那麼沒必要爲此擔憂,JQuery3
已經全面改寫了動畫模塊,採用了requestAnimationFrame()
函數來實現動畫效果。但若是你使用的是以前版本的JQuery
,那麼就須要jquery-requestAnimationFrame來將setTimeout()
替換爲requestAnimationFrame()
函數。
讀到這裏,想必必定會對requestAnimationFrame()
產生好奇。要想獲得一個流暢的動畫,咱們但願讓視覺變化發生在每一幀的開頭,而保證JavaScript
在幀開始時運行的方式則是使用requestAnimationFrame()
函數,本質上它與setTimeout()
沒有什麼區別,都是在遞歸調用同一個回調函數來不斷更新畫面以達到動畫的效果,requestAnimationFrame()
的使用方法以下:
function updateScreen(time) {
// 這是你的動畫效果函數
}
// 將你的動畫效果函數放入requestAnimationFrame()做爲回調函數
requestAnimationFrame(updateScreen);複製代碼
並非全部瀏覽器都支持requestAnimationFrame()
函數,如IE9
(又是萬惡的IE
),但基本上現代瀏覽器都會支持這個功能的,若是你須要兼容老舊版本的瀏覽器,可使用如下函數。
// 本段代碼截取自Paul Irish : https://gist.github.com/paulirish/1579671
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|| window[vendors[x]+'CancelRequestAnimationFrame'];
}
// 若是瀏覽器不支持,則使用setTimeout()
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}());複製代碼
咱們知道JavaScript
是單線程的,但瀏覽器可不是單線程的。JavaScript
在瀏覽器的主線程上運行,這剛好與樣式計算、佈局等許多其餘狀況下的渲染操做一塊兒運行,若是JavaScript
的運行時間過長,就會阻塞這些後續工做,致使幀丟失。
使用Chrome
開發者工具的Timeline
功能能夠幫助咱們查看每一個JavaScript
腳本的運行時間(包括子腳本),幫助咱們發現並突破性能瓶頸。
在找到影響性能的JavaScript
腳本後,咱們能夠經過Web Workers
進行優化。Web Workers
是HTML5
提出的一個標準,它可讓JavaScript
腳本運行在後臺線程(相似於建立一個子線程),然後臺線程不會影響到主線程中的頁面。不過,使用Web Workers
建立的線程是不能操做DOM
樹的(這也是Web Workers
沒有顛覆JavaScript
是單線程的緣由,JavaScript
之因此一直是單線程設計主要也是由於爲了不多個腳本操做DOM
樹的同步問題,這會提升不少複雜性),因此它只適合於作一些純計算的工做(數據的排序、遍歷等)。
若是你的JavaScript
必需要在主線程中執行,那麼只能選擇另外一種方法。將一個大任務分割爲多個小任務(每一個佔用時間不超過幾毫秒),而且在每幀的requestAnimationFrame()
函數中運行:
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
var taskFinishTime;
do {
// 從列表中彈出任務
var nextTask = taskList.pop();
// 執行任務
processTask(nextTask);
// 若是有足夠的時間進行下一個任務則繼續執行
taskFinishTime = window.performance.now();
} while (taskFinishTime - taskStartTime < 3);
if (taskList.length > 0)
requestAnimationFrame(processTaskList);
}複製代碼
建立一個Web Workers
對象很簡單,只須要調用Worker()
構造器,而後傳入指定腳本的URI
。現代主流瀏覽器均支持Web Workers
,除了Internet Explorer
(又是萬惡的IE),因此咱們在下面的示例代碼中還須要檢測瀏覽器是否兼容。
var myWorker;
if (typeof(Worker) !== "undefined") {
// 支持Web Workers
myWorker = new Worker("worker.js");
} else {
// 不支持Web Workers
}複製代碼
Web Workers
與主線程之間經過postMessage()
函數來發送信息,使用onmessage()
事件處理函數來響應消息(主線程與子線程之間並無共享數據,只是經過複製數據來交互)。
main.js:
// 在主線程js中發送數據到myWorker綁定的js腳本線程
myWorker.postMessage("Hello,World");
console.log('Message posted to worker');
worker.js:
// onmessage處理函數容許咱們在任什麼時候刻,
// 一旦接收到消息就能夠執行一些代碼,代碼中消息自己做爲事件的data屬性進行使用。
onmessage = function(data) {
console.log("Message received from main script.");
console.log("Posting message back to main script.");
postMessage("Hello~");
}
main.js:
// 主線程使用onmessage接收消息
myWorker.onmessage = function(data) {
console.log("Received message: " + data);
}複製代碼
若是你須要從主線程中馬上終止一個運行中的worker,能夠調用worker的terminate()
函數:
myWorker.terminate();複製代碼
myWorker會被當即殺死,不會有任何機會讓它繼續完成剩下的工做。而在worker線程中也能夠調用close()
函數進行關閉:
close();複製代碼
有關更多的Web Workers
使用方法,請參考Using Web Workers - Web APIs | MDN。
每次修改DOM
和CSS
都會致使瀏覽器從新計算樣式,在不少狀況下還會對頁面或頁面的一部分從新進行佈局計算。
計算樣式的第一部分是建立一組匹配選擇器(用於計算哪些節點應用哪些樣式),第二部分涉及從匹配選擇器中獲取全部樣式規則,並計算出節點的最終樣式。
經過下降選擇器的複雜性能夠提高樣式計算的速度。
下面是一個複雜的CSS
選擇器:
.box:nth-last-child(-n+1) .title {
/* styles */
}複製代碼
瀏覽器若是想要找到應用該樣式的節點,須要先找到有.title
類的節點,而後其父節點正好是負n個子元素+1個帶.box
類的節點。瀏覽器計算此結果可能須要大量的時間,但咱們能夠把選擇器的預期行爲更改成一個類:
.final-box-title {
/* styles */
}複製代碼
咱們只是將CSS
的命名模塊化(下降選擇器的複雜性),而後只讓瀏覽器簡單地將選擇器與節點進行匹配,這樣瀏覽器計算樣式的效率會提高許多。
BEM
是一種模塊化的CSS
命名規範,使用這種方法組織CSS
不只結構上十分清晰,也對瀏覽器的樣式查找提供了幫助。
BEM
其實就是Block,Element,Modifier
,它是一種基於組件的開發方式,其背後的思想就是將用戶界面劃分爲獨立的塊。這樣即便是使用複雜的UI
也能夠輕鬆快速地開發,而且模塊化的方式能夠提升代碼的複用性。
Block
是一個功能獨立的頁面組件(能夠被重用),Block
的命名方式就像寫Class
名同樣。以下面的.button
就是表明<button>
的Block
。
.button {
background-color: red;
}
<button class="button">I'm a button</button>複製代碼
Element
是一個不能單獨使用的Block
的複合部分。能夠認爲Element
是Block
的子節點。
<!-- `search-form`是一個block -->
<form class="search-form">
<!-- 'search-form__input'是'search-form' block中的一個element -->
<input class="search-form__input">
<!-- 'search-form__button'是'search-form' block中的一個element -->
<button class="search-form__button">Search</button>
</form>複製代碼
Modifier
是用於定義Block
或Element
的外觀、狀態或行爲的實體。假設,咱們有了一個新的需求,對button
的背景顏色使用綠色,那麼咱們可使用Modifier
對.button
進行一次擴展:
.button {
background-color: red;
}
.button--secondary {
background-color: green;
}複製代碼
第一次接觸BEM
的童鞋可能會對這種命名方式感到奇怪,但BEM
重要的是模塊化與可維護性的思想,至於命名徹底能夠按照你所能接受的方式修改。限於篇幅,本文就再也不繼續探討BEM
了,感興趣的童鞋能夠去看BEM的官方文檔。
瀏覽器每次進行佈局計算時幾乎老是會做用到整個DOM
,若是有大量元素,那麼將會須要很長時間才能計算出全部元素的位置與尺寸。
因此咱們應當儘可能避免在運行時動態地修改幾何屬性(寬度、高度等),由於這些改動都會致使瀏覽器從新進行佈局計算。若是沒法避免,那麼要優先使用Flexbox
,它會盡可能減小布局所需的開銷。
強制同步佈局就是使用JavaScript
強制瀏覽器提早執行佈局。須要先明白一點,在JavaScript
運行時,來自上一幀的全部舊佈局值都是已知的。
如下代碼爲例,它在每一幀的開頭輸出了元素的高度:
requestAnimationFrame(logBoxHeight);
function logBoxHeight() {
console.log(box.offsetHeight);
}複製代碼
但若是在請求高度以前,修改了其樣式,就會出現問題,瀏覽器必須先應用樣式,而後進行佈局計算,以後才能返回正確的高度。這是沒必要要的且會產生很是大的開銷。
function logBoxHeight() {
box.classList.add('super-big');
console.log(box.offsetHeight);
}複製代碼
正確的作法,應該利用瀏覽器可使用上一幀佈局值的特性,而後再執行任何寫操做:
function logBoxHeight() {
console.log(box.offsetHeight);
box.classList.add('super-big');
}複製代碼
若是連續不斷地發生強制同步佈局,那麼就會產生布局抖動。如下代碼循環處理一組段落,並設置每一個段落的寬度以匹配一個名爲「box」的元素的寬度。
function resizeAllParagraphsToMatchBlockWidth() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}複製代碼
這段代碼的問題在於每次迭代都會讀取box.offsetWidth
,而後當即使用此值來更新段落的寬度。在循環的下次迭代中,瀏覽器必須考慮樣式更新這一事實(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進行批量讀取與寫入,它能夠防止強制佈局同步與佈局抖動。
在像素管道一節中,咱們發現有種屬性修改後會跳過佈局與繪製階段,這顯然會減小很多性能開銷。目前只有兩種屬性符合這個條件:transform
和opacity
。
須要注意的是,使用transform
和opacity
時,更改這些屬性所在的元素應處於其自身的圖層,因此咱們須要將設置動畫的元素單獨新建一個圖層(這樣作的好處是該圖層上的重繪能夠在不影響其餘圖層上元素的狀況下進行處理。若是你用過Photoshop
,想必可以理解多圖層工做的方便之處)。
建立新圖層的最佳方式是使用will-change
屬性,該屬性告知瀏覽器該元素會有哪些變化,這樣瀏覽器能夠在元素屬性真正發生變化以前提早作好對應的優化準備工做。
.moving-element {
will-change: transform;
}
// 對於不支持 will-change 但受益於層建立的瀏覽器,須要使用(濫用)3D 變形來強制建立一個新層
.moving-element {
transform: translateZ(0);
}複製代碼
但不要認爲will-change
能夠提升性能就隨便濫用,使用will-change
進行預優化與建立圖層都須要額外的內存和管理開銷,隨便濫用只會得不償失。