如何提升頁面渲染效率
Web頁面的性能
我們每天都會瀏覽很多的Web頁面,使用很多基於Web的應用。這些站點看起來既不一樣,用途也都各有不同,有在線視頻,Social Media,新聞,郵件客戶端,在線存儲,甚至圖形編輯,地理信息系統等等。雖然有著各種各樣的不同,但是相同的是,他們背後的工作原理都是一樣的:
-
用戶輸入網址
-
瀏覽器加載HTML/CSS/JS,圖片資源等
-
瀏覽器將結果繪制成圖形
-
用戶通過鼠標,鍵盤等與頁面交互
這些種類繁多的頁面,在用戶體驗方面也有很大差異:有的響應很快,用戶很容易就可以完成自己想要做的事情;有的則慢慢吞吞,讓焦躁的用戶在受挫之後拂袖而去。毫無疑問,性能是影響用戶體驗的一個非常重要的因素,而影響性能的因素非常非常多,從用戶輸入網址,到用戶最終看到結果,需要有很多的參與方共同努力。這些參與方中任何一個環節的性能都會影響到用戶體驗。
-
寬帶網速
-
DNS服務器的響應速度
-
服務器的處理能力
-
數據庫性能
-
路由轉發
-
瀏覽器處理能力
早在2006年,雅虎就發布了提升站點性能的指南,Google也發布了類似的指南。而且有很多工具可以和瀏覽器一起工作,對你的Web頁面的加載速度進行評估:分析頁面中資源的數量,傳輸是否采用了壓縮,JS、CSS是否進行了精簡,有沒有合理的使用緩存等等。
如果你需要將這個過程與CI集成在一起,來對應用的性能進行監控,我去年寫過一篇相關的博客。
本文打算從另一個角度來嘗試加速頁面的渲染:瀏覽器是如何工作的,要將一個頁面渲染成用戶可以看到的圖形,瀏覽器都需要做什麽,哪些過程比較耗時,以及如何避免這些過程(或者至少以更高效的方式)。
頁面是如何被渲染的
說到性能優化,規則一就是:
If you can’t measure it, you can’t improve it. – Peter Drucker
根據瀏覽器的工作原理,我們可以分別對各個階段進行度量。
圖片來源:http://dietjs.com/tutorials/host#backend
像素渲染流水線
-
下載HTML文檔
-
解析HTML文檔,生成DOM
-
下載文檔中引用的CSS、JS
-
解析CSS樣式表,生成CSSOM
-
將JS代碼交給JS引擎執行
-
合並DOM和CSSOM,生成Render Tree
-
根據Render Tree進行布局layout(為每個元素計算尺寸和位置信息)
-
繪制(Paint)每個層中的元素(繪制每個瓦片,瓦片這個詞與GIS中的瓦片含義相同)
-
執行圖層合並(Composite Layers)
使用Chrome的DevTools – Timing,可以很容易的獲取一個頁面的渲染情況,比如在Event Log頁簽上,我們可以看到每個階段的耗時細節(清晰起見,我沒有顯示Loading和Scripting的耗時):
上圖中的Activity中,Recalculate Style就是上面的構建CSSOM的過程,其余Activity都分別於上述的過程匹配。
應該註意的是,瀏覽器可能會將Render Tree分成好幾個層來分別繪制,最後再合並起來形成最終的結果,這個過程一般發生在GPU中。
Devtools中有一個選項:Rendering - Layers Borders,打開這個選項之後,你可以看到每個層,每個瓦片的邊界。瀏覽器可能會啟動多個線程來繪制不同的層/瓦片。
Chrome還提供一個Paint Profiler的高級功能,在Event Log中選擇一個Paint,然後點擊右側的Paint Profiler就可以看到其中繪制的全過程:
你可以拖動滑塊來看到隨著時間的前進,頁面上元素被逐步繪制出來了。我錄制了一個我的知乎活動頁面的視頻,不過需要FQ。
視頻地址:https://youtu.be/gley7VZFx_I
常規策略
為了盡快的讓用戶看到頁面內容,我們需要快速的完成DOM+CSSOM - Layout - Paint - Composite Layers的整個過程。一切會阻塞DOM生成,阻塞CSSOM生成的動作都應該盡可能消除,或者延遲。
在這個前提下,常見的做法有兩種:
分割CSS
對於不同的瀏覽終端,同一終端的不同模式,我們可能會提供不同的規則集:
@media print {
html {
font-family: ‘Open Sans‘;
font-size: 12px;
}
}
@media orientation:landscape {
//
}
如果將這些內容寫到統一個文件中,瀏覽器需要下載並解析這些內容(雖然不會實際應用這些規則)。更好的做法是,將這些內容通過對link元素的media屬性來指定:
<link href="print.css" rel="stylesheet" media="print">
<link href="landscape.css" rel="stylesheet" media="orientation:landscape">
這樣,print.css和landscape.css的內容不會阻塞Render Tree的建立,用戶可以更快的看到頁面,從而獲得更好的體驗。
高效的CSS規則
CSS規則的優先級
很多使用SASS/LESS的開發人員,太過分的喜愛嵌套規則的特性,這可能會導致復雜的、無必要深層次的規則,比如:
#container {
p {
.title {
span {
color: #f3f3f3;
}
}
}
}
在生成的CSS中,可以看到:
#container p .title span {
color: #f3f3f3;
}
而這個層次可能並非必要。CSS規則越復雜,在構建Render Tree時,瀏覽器花費的時間越長。CSS規則有自己的優先級,不同的寫法對效率也會有影響,特別是當規則很多的時候。這裏有一篇關於CSS規則優先級的文章可供參考。
使用GPU加速
很多動畫都會定時執行,每次執行都可能會導致瀏覽器的重新布局,比如:
@keyframes my {
20% {
top: 10px;
}
50% {
top: 120px;
}
80% {
top: 10px;
}
}
這些內容可以放到GPU加速執行(GPU是專門設計來進行圖形處理的,在圖形處理上,比CPU要高效很多)。可以通過使用transform來啟動這一特性:
@keyframes my {
20% {
transform: translateY(10px);
}
50% {
transform: translateY(120px);
}
80% {
transform: translateY(10px);
}
}
異步JavaScript
我們知道,JavaScript的執行會阻塞DOM的構建過程,這是因為JavaScript中可能會有DOM操作:
var element = document.createElement(‘div‘);
element.style.width = ‘200px‘;
element.style.color = ‘blue‘;
body.appendChild(element);
因此瀏覽器會等等待JS引擎的執行,執行結束之後,再恢復DOM的構建。但是並不是所有的JavaScript都會設計DOM操作,比如審計信息,WebWorker等,對於這些腳本,我們可以顯式地指定該腳本是不阻塞DOM渲染的。
<script src="worker.js" async></script>
帶有async標記的腳本,瀏覽器仍然會下載它,並在合適的時機執行,但是不會影響DOM樹的構建過程。
首次渲染之後
在首次渲染之後,頁面上的元素還可能被不斷的重新布局,重新繪制。如果處理不當,這些動作可能會產生性能問題,產生不好的用戶體驗。
-
訪問元素的某些屬性
-
通過JavaScript修改元素的CSS屬性
-
在onScroll中做耗時任務
-
圖片的預處理(事先裁剪圖片,而不是依賴瀏覽器在布局時的縮放)
-
在其他Event Handler中做耗時任務
-
過多的動畫
-
過多的數據處理(可以考慮放入WebWorker內執行)
強制同步布局/回流
元素的一些屬性和方法,當在被訪問或者被調用的時候,會觸發瀏覽器的布局動作(以及後續的Paint動作),而布局基本上都會波及頁面上的所有元素。當頁面元素比較多的時候,布局和繪制都會花費比較大。
通過Timeline,有時候你會看到這樣的警告:
比如訪問一個元素的offsetWidth(布局寬度)屬性時,瀏覽器需要重新計算(重新布局),然後才能返回最新的值。如果這個動作發生在一個很大的循環中,那麽瀏覽器就不得不進行多次的重新布局,這可能會產生嚴重的性能問題:
for(var i = 0; i < list.length; i++) {
list[i].style.width = parent.offsetWidth + ‘px‘;
}
正確的做法是,先將這個值讀出來,然後緩存在一個變量上(觸發一次重新布局),以便後續使用:
var parentWidth = parent.offsetWidth;
for(var i = 0; i < list.length; i++) {
list[i].style.width = parentWidth + ‘px‘;
}
CSS樣式修改
布局相關屬性修改
修改布局相關屬性,會觸發Layout - Paint - Composite Layers,比如對位置,尺寸信息的修改:
var element = document.querySelector(‘#id‘);
element.style.width = ‘100px‘;
element.style.height = ‘100px‘;
element.style.top = ‘20px‘;
element.style.left = ‘20px‘;
繪制相關屬性修改
修改繪制相關屬性,不會觸發Layout,但是會觸發後續的Paint - Composite Layers,比如對背景色,前景色的修改:
var element = document.querySelector(‘#id‘);
element.style.backgroundColor = ‘red‘;
其他屬性
除了上邊的兩種之外,有一些特別的屬性可以在不同的層中單獨繪制,然後再合並圖層。對這種屬性的訪問(如果正確使用了CSS)不會觸發Layout - Paint,而是直接進行Compsite Layers:
-
transform
-
opacity
transform展開的話又分為: translate, scale, rotate等,這些層應該放入單獨的渲染層中,為了對這個元素創建一個獨立的渲染層,你必須提升該元素。
可以通過這樣的方式來提升該元素:
.element {
will-change: transform;
}
CSS 屬性 will-change 為web開發者提供了一種告知瀏覽器該元素會有哪些變化的方法,這樣瀏覽器可以在元素屬性真正發生變化之前提前做好對應的優化準備工作。
當然,額外的層次並不是沒有代價的。太多的獨立渲染層,雖然縮減了Paint的時間,但是增加了Composite Layers的時間,因此需要仔細權衡。在作調整之前,需要Timeline的運行結果來做支持。
還記得性能優化的規則一嗎?
If you can’t measure it, you can’t improve it. – Peter Drucker
下面這個視頻裏可以看到,當鼠標挪動到特定元素時,由於CSS樣式的變化,元素會被重新繪制:
視頻地址:https://youtu.be/c6wKfDpbwu8
CSS Triggers是一個完整的CSS屬性列表,其中包含了會影響布局或者繪制的CSS屬性,以及在不同的瀏覽器上的不同表現。
總結
了解瀏覽器的工作方式,對我們做前端頁面渲染性能的分析和優化都非常有幫助。為了高效而智能的完成渲染,瀏覽器也在不斷的進行優化,比如資源的預加載,更好的利用GPU(啟用更多的線程來渲染)等等。
另一方面,我們在編寫前端的HTML、JS、CSS時,也需要考慮瀏覽器的現狀:如何減少DOM、CSSOM的構建時間,如何將耗時任務放在單獨的線程中(通過WebWorker)。
參考資料
-
Google出品的Web基礎
-
一篇關於如何優化CSS的文章
-
CSSOM的介紹
如何提升頁面渲染效率