JS阻塞渲染,這么多年我理解錯啦?
大家好,我卡頌。
在中文社區(qū),這么多年一直流傳一個說法:
JS線程負責執(zhí)行JS,GUI渲染線程負責渲染,這兩者是互斥的,所以JS執(zhí)行時會阻塞渲染。
但隨著Dev Tools使用的增多,逐漸開始懷疑以上說法。本文會以實際案例來解釋為什么JS阻塞渲染。
到底幾個線程
在講解JS線程與GUI線程互斥的文章中,通常會列出渲染進程包含的線程,比如:
- GUI渲染線程。
- JS引擎線程。
- 事件觸發(fā)線程。
- 定時觸發(fā)器線程。
- HTTP請求線程。
但是,我們以百度的搜索頁舉例,打開Performance面板開啟錄制:
上圖錄制結果中:
- Chrome_ChildIOThread對應IO線程的任務記錄,用戶輸入、網絡、設備相關事件都與他相關。
- Raster記錄光柵化線程池任務、GPU記錄GPU合成位圖的任務、Compositor記錄合成線程的任務執(zhí)行,以上三者都與瀏覽器渲染相關。
- Main記錄渲染進程的主線程中的任務。
從這個角度看,瀏覽器實際的線程情況與那些GUI線程相關的文章描述的并不相同。
主線程的任務
接下來,讓我們進入Main。紅線框內長短不一的灰色塊,就是主線程中執(zhí)行的任務。
注意看紅框內的綠色塊FP,代表First Paint(首次繪制):
那么在首次繪制前都要執(zhí)行什么任務呢?可以看到主要有3個Task(任務):
第一個任務是請求HTML數據:
Parse HTML
當請求回HTML字節(jié)流后,開始第二個任務,將HTML字節(jié)流解析為DOM,這個任務的名字就是圖中的藍色塊Parse HTML:
注意其中有些執(zhí)行時長不一的Evaluate Script,這些是解析DOM樹過程中遇到的JS代碼。
從DOM樹中可以看到這些阻塞DOM樹生成的JS腳本:
他們的存在顯著拉長了Parse HTML的用時。
Recaculate Style
解析完DOM樹(藍色Parse HTML)后,下一個任務是紫色Recaculate Style:
他負責將HTML中的CSS樣式(外聯、內聯)輸出為styleSheets,styleSheets有兩個作用:
- 可以與DOM樹結合為頁面帶來樣式。
- JS可以操作styleSheets改變頁面樣式。
我們可以從控制臺打印document.styleSheets直觀感受他的存在:
Layout
有了DOM樹與styleSheets,接下來需要為視圖中可見部分生成一棵樹(比如display: none部分就不需要在這棵樹中顯示)。
這個任務是紫色Layout:
Update Layer Tree
用戶看到的頁面實際是由多層頁面重疊后的結果,開發(fā)者可以用很多手段(比如z-index)改變某部分的層級。
比如滾動條就會形成自己獨立的層級:
既然是多層結構,那么就需要更新每層的信息,這個任務是紫色的Update Layer Tree:
Paint
我們可以發(fā)現,在FP之前,Update Layer Tree之后只剩下Paint這一任務了:
從字面意義講,這就是「繪制」么?并不是。
Paint的任務是整理每一層頁面的繪制信息,構成繪制列表,這些數據會交給合成線程負責后續(xù)繪制操作。
可以發(fā)現,具體的繪制操作是交由合成線程完成,他與JS所在線程(主線程)并不是互斥的。
JS為啥阻塞渲染
我們現在知道,JS執(zhí)行與Paint任務都發(fā)生在主線程。
「渲染被阻塞」的原因很明顯:因為Paint任務沒有及時執(zhí)行,即繪制列表沒有及時提交給合成線程。
之所以沒有及時執(zhí)行,可能是因為JS執(zhí)行時間過長,導致這一幀沒有時間執(zhí)行Paint。
比如,我們打開B站,記錄下主線程的任務。
可以看到,有個JS執(zhí)行時長達到231.88ms,超過了一幀的時間,在此期間主線程就沒時間執(zhí)行Paint了:
總結
JS之所以阻塞渲染,是因為JS執(zhí)行與「渲染相關任務」都在爭奪主線程有限的資源。
當JS執(zhí)行時間過長,「渲染相關任務」就沒時間執(zhí)行了。