EventLoop = TaskQueue + RenderQueue,你看明白了嗎?
前言
在最近的工作和學(xué)習(xí)中,有一個詞總是在眼前揮之不去--EventLoop。而在之前,其實我們講過相關(guān)的內(nèi)容,Event Loop 可視化解析
圖片
上文我們從偏JS調(diào)用機制的角度分析了,調(diào)用棧(Call Stack)/宏任務(wù)隊列 (Task Queue)和微任務(wù)隊列 (Microtask Queue)他們之間的關(guān)系和他們是如何協(xié)同合作的。并且,舉了很多例子,用可視化的方式講解它們?nèi)绾喂ぷ鞯摹?/p>
而今天,我們從瀏覽器內(nèi)部的實現(xiàn)細節(jié)來談?wù)凟ventLoop是如何從接受任務(wù)到渲染出對應(yīng)頁面的。
也就是下圖中所涉及到的各個重要節(jié)點。在閱讀完本文后,希望大家能對下面有一個清晰的認知。
圖片
好了,天不早了,干點正事哇。
我們能所學(xué)到的知識點
- 前置知識點
- 事件循環(huán)(Event Loop)
- 任務(wù)隊列/微任務(wù)隊列/調(diào)用棧
- 在渲染隊列中執(zhí)行的是什么?
- EventLoop模型
1. 前置知識點
「前置知識點」,只是做一個概念的介紹,不會做深度解釋。因為,這些概念在下面文章中會有出現(xiàn),為了讓行文更加的順暢,所以將本該在文內(nèi)的概念解釋放到前面來?!溉绻蠹覍@些概念熟悉,可以直接忽略」同時,由于閱讀我文章的群體有很多,所以有些知識點可能「我視之若珍寶,爾視只如草芥,棄之如敝履」。以下知識點,請「酌情使用」。
頁面刷新術(shù)語
我們在頁面是如何生成的(宏觀角度)一文中提到過這些指標,這里就拿來主義了。
- 「屏幕刷新頻率」
一秒內(nèi)屏幕刷新的次數(shù)(一秒內(nèi)顯示了多少幀的圖像),單位 Hz(赫茲),如常見的 60 Hz?!杆⑿骂l率取決于硬件的固定參數(shù)」(不會變的)。
- 「逐行掃描」
顯示器并不是一次性將畫面顯示到屏幕上,而是「從左到右邊,從上到下逐行掃描」,順序顯示整屏的一個個像素點,不過這一過程快到人眼無法察覺到變化。
以 60 Hz 刷新率的屏幕為例,這一過程即 1000 / 60 ≈ 16ms。
當掃描完一個屏幕后,設(shè)備需要「重新回到第一行」以進入下一次的循環(huán),此時有一段時間空隙,稱為VerticalBlanking Interval(VBI)。
「幀率 (Frame Rate)」
表示 「GPU 在一秒內(nèi)繪制操作的幀數(shù)」,如 60 fps,即每秒鐘GPU最多繪制 60 幀畫面。
幀率是「動態(tài)變化」的,例如當畫面靜止時,GPU 是沒有繪制操作的,屏幕刷新的還是buffer中的數(shù)據(jù),即GPU最后操作的幀數(shù)據(jù)。
「畫面撕裂(tearing)」
一個屏幕內(nèi)的數(shù)據(jù)來自2個不同的幀,畫面會出現(xiàn)撕裂感。
測試幀率
我們可以借助requestAnimationFrame通過每個測量前后幀發(fā)生的時間間隔,來從側(cè)面查看本地瀏覽器幀率。
const checkRequestAnimationDiff = () => {
let prev;
function call() {
requestAnimationFrame((timestamp) => {
if (prev) {
console.log(timestamp - prev);
// 應(yīng)該大約是60FPS的16.6毫秒
}
prev = timestamp;
call();
});
}
call();
}
checkRequestAnimationDiff();
隨意打開一個網(wǎng)站,并將上述代碼貼到devtool-Console運行。
下面是,我們在React-官網(wǎng)[1]中實驗的結(jié)果。
圖片
從輸出結(jié)果來看,雖然結(jié)果不是唯一,但是它們的值都穩(wěn)定在16.67~16.68。和我們60fps是吻合的。
WebAPI
WebAPI工作的原理依賴于瀏覽器作為宿主環(huán)境來提供和執(zhí)行這些API。在Web開發(fā)中,我們通常指的WebAPI是「瀏覽器內(nèi)置的API」,它們允許開發(fā)者利用JavaScript與瀏覽器的功能進行交互。
APIs | 描述 |
網(wǎng)絡(luò)請求 (Network Requests) | 使用 |
DOM操作 (DOM Manipulation) | 瀏覽器提供了一套DOM API,允許 |
事件處理 (Event Handling) | WebAPI允許注冊事件處理程序來響應(yīng)用戶行為(如點擊、滑動)或瀏覽器事件(如頁面加載、窗口尺寸變化)。 |
存儲機制 (Storage Mechanisms) | 瀏覽器提供了如 |
設(shè)備API (Device APIs) | 可以訪問設(shè)備的硬件,如攝像頭、麥克風(fēng)、地理位置等,這通常通過 |
圖形和動畫 (Graphics & Animation) |
|
性能監(jiān)控 (Performance Monitoring) |
|
其他API (Other APIs) | 還有諸如 |
WebAPI的工作流程
- 「調(diào)用API」:開發(fā)者在JavaScript代碼中調(diào)用某個WebAPI。
- 「瀏覽器解釋執(zhí)行」: 瀏覽器解釋JavaScript代碼,并「執(zhí)行相應(yīng)的API調(diào)用」。
- 「API內(nèi)部處理」:WebAPI內(nèi)部可能會執(zhí)行多種操作,如觸發(fā)網(wǎng)絡(luò)請求、訪問數(shù)據(jù)庫、啟動硬件設(shè)備等。
- 「回調(diào)和事件循環(huán)」:對于異步操作,WebAPI通常會使用回調(diào)函數(shù)或Promise來處理操作完成后的結(jié)果。瀏覽器的事件循環(huán)機制確保了這些回調(diào)在適當?shù)臅r候被調(diào)用。
- 「渲染和更新」:對于涉及視覺變化的API,如DOM操作或Canvas繪圖,瀏覽器會更新頁面內(nèi)容,這通常發(fā)生在瀏覽器的下一個重繪周期。
在整個過程中,「瀏覽器的角色是中介」,它提供了執(zhí)行API的環(huán)境和必要的安全措施。這些API讓W(xué)eb應(yīng)用可以像本地應(yīng)用一樣豐富和強大,同時仍然運行在瀏覽器這個相對安全的沙箱環(huán)境中。
下面的圖,展示了WebAPI的地位(中間部分)。
圖片
GPU硬件加速
「GPU(Graphics Processing Unit)硬件加速」是一種利用GPU來執(zhí)行圖形和計算任務(wù)的技術(shù)。在Web開發(fā)中,GPU硬件加速可以通過利用用戶計算機中的GPU資源來加速瀏覽器的渲染和繪制操作。這通??梢蕴岣呔W(wǎng)頁的性能和流暢度,尤其是對于需要大量圖形操作的頁面。
在Web開發(fā)中,一些CSS屬性和操作可以觸發(fā)GPU硬件加速,以便更有效地利用GPU資源。
- 3D 變換(transform)
使用transform屬性進行3D變換,如translate3d、rotate3d、scale3d等,可以觸發(fā)GPU硬件加速。例如:
.element {
transform: translate3d(0, 0, 0);
}
- CSS 動畫(animation)和過渡(transition):
使用CSS動畫和過渡屬性,例如transform屬性的過渡,可以觸發(fā)GPU硬件加速。例如:
.element { transition: transform 0.3s ease-in-out; }
Canvas 繪圖:
在<canvas>元素上進行繪圖操作通常會利用GPU硬件加速。這包括使用2D或WebGL上下文進行圖形渲染。
使用 will-change 屬性:
will-change屬性告訴瀏覽器某個屬性將會被改變,從而可以提前進行優(yōu)化。例如:
.element { will-change: transform; }
使用 image-rendering 屬性:
image-rendering屬性用于指定圖像的渲染質(zhì)量,而且在某些情況下也能觸發(fā)GPU硬件加速。例如:
.element { image-rendering: pixelated; }
使用 backface-visibility 屬性:
backface-visibility屬性用于指定當元素不面向屏幕時是否可見。在某些情況下,該屬性的使用可以觸發(fā)GPU硬件加速。例如:
.element { backface-visibility: hidden; }
使用 filter 屬性(某些情況下):
在某些情況下,使用filter屬性(如模糊、對比度等)可能觸發(fā)GPU硬件加速。
還記得我們在你會在瀏覽器中打斷點嗎?我會!中介紹過如何看chromium 在線倉庫[2]
那我們就從源碼的角度來看看,為什么上面的屬性會走GPU硬件加速
或者我們可以看compositing_reason_finder.cc這個文件,它例舉了很多枚舉類型。
圖片
2. 事件循環(huán)(Event Loop)
事件循環(huán)就是一個「死循環(huán)」,不死不休。
舊的操作系統(tǒng)不支持多線程,它們的事件循環(huán)可以被大致描述為一個簡單的循環(huán):
while (true) {
if (hasTasks()) {
executeTask();
}
}
現(xiàn)代操作系統(tǒng)的調(diào)度器(schedulers)非常復(fù)雜。它們有優(yōu)先級設(shè)置、執(zhí)行隊列等許多其他技術(shù)。
這里做一個題外話,看到schedulers/優(yōu)先級設(shè)置是不是想到React-Fiber架構(gòu)了。其實,React在內(nèi)部就是模仿操作系統(tǒng),做了自己的實現(xiàn)邏輯。(這里就不展開說明了)
為了讓事情簡單化,我們可以將事件循環(huán)(Event Loop)描述為一個循環(huán),該循環(huán)檢查是否有任何待處理的任務(wù):
圖片
任務(wù)觸發(fā)器
瀏覽器屬于「事件驅(qū)動」的技術(shù)框架,如果想讓Event Loop探查并執(zhí)行對應(yīng)的任務(wù),首先要做的就是將某些任務(wù)進行觸發(fā)。也就是喚起指定任務(wù)的觸發(fā)器。
下面就是我們平時能夠接觸到的任務(wù)觸發(fā)器
- 「<script>標簽」:通過HTML的<script>標簽引入的代碼會被瀏覽器解析并執(zhí)行,相關(guān)的同步任務(wù)會被放入事件循環(huán)中。
- 「延后的任務(wù)」:
setTimeout:設(shè)置一個計時器,在指定的延時后執(zhí)行一段代碼。
setInterval:設(shè)置一個計時器,按照指定的時間間隔重復(fù)執(zhí)行一段代碼。
requestIdleCallback:安排一個函數(shù)在瀏覽器空閑時期被調(diào)用。
- 「瀏覽器API的事件處理程序」:
用戶觸發(fā)的事件,例如click, mousedown, input, blur等。
代碼生成的事件,比如XMLHttpRequest的響應(yīng)處理、fetch API的promise resolve等。
「Promise狀態(tài)變化」:當一個Promise對象的狀態(tài)改變時(例如從pending變?yōu)閒ulfilled或rejected),相關(guān)的任務(wù)會被加入事件循環(huán)。
「觀察者」:
DOMMutationObserver:用于觀察DOM變動,當DOM發(fā)生變化時可以通知應(yīng)用。
IntersectionObserver:用于觀察元素是否進入了父元素或視口的特定區(qū)域。
requestAnimationFrame:用于在「下一次重新渲染前」執(zhí)行動畫或視覺更新的函數(shù),使動畫流暢。
上面的任務(wù)幾乎都是通過WebAPI進行觸發(fā)的。
例如,我們在代碼中有這樣一行:setTimeout(function a() {}, 100)。當我們執(zhí)行setTimeout時,WebAPI將任務(wù)延遲了100毫秒。100毫秒后,WebAPI將函數(shù)a()放入任務(wù)隊列(Task Queue)(也可以稱為Callback Queue)中。事件循環(huán)在下一個循環(huán)中獲取該任務(wù)并執(zhí)行它。
JS執(zhí)行和頁面渲染是難兄難弟
EventLoop = TaskQueue + RenderQueue
在之前的文章中,我們提到過文檔對象模型(DOM)是一個應(yīng)用編程接口(API),通過創(chuàng)建表示文檔的樹,以一種「獨立于平臺和語言」的方式訪問和修改一個頁面的內(nèi)容和結(jié)構(gòu)。
在HTML文檔中,Web開發(fā)者可以使用JS來CRUD DOM 結(jié)構(gòu),其主要的目的是「動態(tài)」改變HTML文檔的結(jié)構(gòu)。
- 讀取DOM元素的數(shù)據(jù):大小、屬性、位置等
- 改變屬性:data-屬性、寬度、高度、位置、CSS屬性等
- 創(chuàng)建/刪除HTML節(jié)點
而在JS把玩DOM之后,就將其扔給了瀏覽器的渲染引擎,而渲染引擎任勞任怨的處理DOM和DOM攜帶的附帶信息,并將其渲染成用戶心儀的頁面。
也就是說「JS和瀏覽器(渲染引擎)都能染指過DOM」。
基于上面的特定的背景,我們可以得出一個結(jié)論,執(zhí)行JS和渲染頁面都在同一個線程里。
這意味著事件循環(huán)包含渲染流程。而渲染流程不是單一的操作。其實,它是渲染隊列:
圖片
現(xiàn)在EventLoop處理鏈路上有兩個任務(wù)源。
- JS任務(wù) - SomeJsTasks
- 渲染任務(wù) - RenderQueue
屏幕更新
對于瀏覽器來說,事件循環(huán)與幀(frames)密切相關(guān),因為EventLoop同時執(zhí)行JS代碼并渲染頁面。
可以將幀視為屏幕狀態(tài)的快照,即用戶在某一時刻看到的畫面。
圖片
我們在Chromium 最新渲染引擎--RenderingNG也介紹過frames。
圖片
瀏覽器的目標是盡快顯示頁面更新,考慮到硬件和軟件的限制:
- 硬件限制:屏幕刷新率
- 軟件限制:操作系統(tǒng)設(shè)置、瀏覽器及其設(shè)置、節(jié)能設(shè)置等
絕大多數(shù)瀏覽器/操作系統(tǒng)支持60幀每秒(Frames Per Second,F(xiàn)PS)。瀏覽器試圖以這個特定的速率更新屏幕。
當我們使用60 FPS時,這意味著瀏覽器在必須「渲染新幀之前有16.6毫秒的時間段來執(zhí)行任務(wù)」(1000/60),而渲染新幀也會消耗時間。
3. 任務(wù)隊列/微任務(wù)隊列/調(diào)用棧
瀏覽器使用兩個隊列來執(zhí)行我們的JS代碼:
- 任務(wù)隊列(TaskQueue)或宏任務(wù)隊列專用于所有事件、延遲任務(wù)等。
- 微任務(wù)隊列(Microtask Queue)用于處理 promise 回調(diào)(已解決和已拒絕),以及 MutationObserver。這個隊列中的單個元素被稱為 微任務(wù)。
任務(wù)隊列(Task Queue)
當瀏覽器收到一個新任務(wù)時,它將任務(wù)放入任務(wù)隊列。每個事件循環(huán)定期從任務(wù)隊列中獲取任務(wù)并執(zhí)行它。任務(wù)完成后,「如果瀏覽器有時間(渲染隊列沒有任務(wù)),事件循環(huán)從任務(wù)隊列獲取另一個任務(wù),直到渲染隊列接收到任務(wù)為止」。
案例分析1
圖片
我們有3個任務(wù):A、B、C。事件循環(huán)獲取并執(zhí)行第一個任務(wù),花費了4毫秒。然后事件循環(huán)檢查其他隊列(微任務(wù)隊列和渲染隊列),它們是空的。事件循環(huán)執(zhí)行任務(wù)B,花費了12毫秒??偣矁蓚€任務(wù)使用了16毫秒。然后瀏覽器將任務(wù)添加到渲染隊列以繪制新幀。事件循環(huán)檢查渲染隊列并開始執(zhí)行渲染隊列中的任務(wù),它們大約花費1毫秒。完成這些操作后,事件循環(huán)返回到任務(wù)隊列并執(zhí)行最后一個任務(wù)C。
事件循環(huán)無法預(yù)測任務(wù)將花費多少時間。此外,事件循環(huán)「無法暫停任務(wù)來渲染幀」,因為瀏覽器引擎不知道該任務(wù)是否會對繪制內(nèi)容有修改動作,還是任務(wù)只是為了渲染幀做了一些無關(guān)痛癢的準備工作。
在執(zhí)行JS代碼期間,「JS所做的所有更改并不會直接呈現(xiàn)給用戶,而是等到宏任務(wù)和所有待處理的微任務(wù)完成后才會表現(xiàn)出來」。但是,此時在JS中可以獲取到最新DOM的變更信息。
案例分析2
圖片
隊列中只有2個任務(wù)(A、B)。第一個任務(wù)A花費了240毫秒(無法中斷)。由于60FPS意味著每16.6毫秒應(yīng)該渲染一幀,所以瀏覽器有14幀的空窗期。當任務(wù)A結(jié)束時,事件循環(huán)執(zhí)行渲染隊列中的任務(wù)以繪制新幀。
「盡管我們失去了14幀,這并不意味著我們將連續(xù)渲染15幀。它只會渲染最后一幀」。
這就是當JS中有長任務(wù)執(zhí)行時,會阻塞頁面的渲染,如果這14幀中間操作了過多的DOM,頁面中就會有一種從第一幀到第十五幀的跳動。這就是為什么我們總是要將長任務(wù)拆分成很多小任務(wù)的原因。
調(diào)用棧(Call Stack)
在Event Loop 可視化解析講解中我們對調(diào)用棧有過介紹。
圖片
案例分析
function D() {
debugger;
console.log('前端柒八九');
}
function C() {
D();
}
function B() {
C();
}
function other() {
// 不在我們考察堆棧上下文中
}
function A() {
const arr = [];
while (arr.length < 2) {
arr.push(other());
}
B();
}
console.log(A());
將上面的代碼貼到devTool-Console或者devTool-Source-Snippet中執(zhí)行。這段代碼將在debugger處暫停。
這里多提一嘴,關(guān)于如何在瀏覽器中優(yōu)雅的調(diào)試斷點,可以參考你會在瀏覽器中打斷點嗎?我會!
- console.log(A());,它是調(diào)用棧的開始。
- 然后我們進入 A 函數(shù),并多次調(diào)用 other。在我們到達debugger之前,這個函數(shù)不會出現(xiàn)在調(diào)用棧中,因為它在我們到達調(diào)試器之前就結(jié)束了。
這是我們在debugger停止時調(diào)用棧的樣子:
圖片
圖中(anonymous)表示全局作用域,我們沒貼出來,它就是指向25行調(diào)用棧的入口的。
當調(diào)用棧為空時,「當前任務(wù)」完成。
微任務(wù)
微任務(wù)只有兩個可能的來源:
- Promise 回調(diào)(onResolved/onRejected)
- MutationObserver 回調(diào)。
微任務(wù)有一個主要特征,使它們與其他任務(wù)完全不同:
一旦調(diào)用棧為空,微任務(wù)將立即執(zhí)行。
微任務(wù)可以創(chuàng)建其他微任務(wù),「這些微任務(wù)將在調(diào)用棧結(jié)束時執(zhí)行」。每個「新的微任務(wù)都會推遲執(zhí)行新的宏任務(wù)或新幀的渲染」。
案例分析
在這個例子中,微任務(wù)隊列中有4個微任務(wù):
圖片
要執(zhí)行的第一個微任務(wù)是A。A花費了200毫秒,而且我們在渲染隊列中有任務(wù)。然而,它們將被推遲,因為我們?nèi)匀挥?個微任務(wù)。這意味著在執(zhí)行A后,事件循環(huán)將執(zhí)行微任務(wù)B、C,最后是D。當「微任務(wù)隊列變空時,事件循環(huán)渲染新幀」。在這個例子中,這4個微任務(wù)花費了0.5秒。在這段時間內(nèi),瀏覽器「UI被阻塞,不可交互」。
后續(xù)的微任務(wù)可以阻塞網(wǎng)站UI,使頁面變得不可交互。
這個微任務(wù)特性既可能是優(yōu)勢也可能是劣勢。例如,當 MutationObserver 根據(jù)DOM更改調(diào)用其回調(diào)時,用戶在回調(diào)完成之前看不到頁面上的更改。因此,我們可以有效地管理用戶看到的內(nèi)容。
更新后的事件循環(huán)圖示:
圖片
各自的特性
- 調(diào)用棧是用于跟蹤「正在被執(zhí)行」函數(shù)的機制,而宏任務(wù)隊列是用于跟蹤「將要被執(zhí)行」函數(shù)的機制。
- 宏任務(wù)隊列和微任務(wù)隊列都是「FIFO」(先進先出)的隊列結(jié)構(gòu),這些任務(wù)是「同步阻塞」的
4. 在渲染隊列中執(zhí)行的是什么?
其實,在瀏覽器中渲染頁面是有很多步驟的。
同時還涉及多個進程之間的通信。這在之前的頁面是如何生成的(宏觀角度)有過介紹,這里就不在羅嗦了。
而今天呢,我們從瀏覽器渲染幀的角度來看到底發(fā)生了啥?!其實幀渲染不是一個單一的操作,它有幾個階段,每個階段都可以分為子階段。
新幀渲染的基本結(jié)構(gòu)
RequestAnimationFrame(RAF)
圖片
requestAnimationFrame 是一個由瀏覽器提供的 JavaScript API,用于在下一次「瀏覽器重繪之前」執(zhí)行指定的函數(shù)。這個函數(shù)通常用于執(zhí)行動畫或其他需要高性能更新的任務(wù),因為它會在瀏覽器的繪制周期內(nèi)運行,以確保動畫的平滑流暢。
特點
- 「與瀏覽器的重繪同步:」 requestAnimationFrame 的執(zhí)行時機與瀏覽器的重繪周期相同,通常是每秒60次(60幀每秒),這確保了動畫的流暢性。
- 「自動暫停:」 當用戶切換到其他標簽頁或最小化瀏覽器時,requestAnimationFrame 會自動暫停,從而節(jié)省系統(tǒng)資源。
- 「避免卡頓:」 由于與瀏覽器的繪制同步,requestAnimationFrame 可以避免由于連續(xù)執(zhí)行任務(wù)導(dǎo)致的卡頓和性能問題。
- RAF的回調(diào)有一個DOMHighResTimeStamp參數(shù),它是自時間起源[3]以來經(jīng)過的毫秒數(shù),即文檔生命周期的開始。我們不需要在回調(diào)中使用performance.now();
- RAF返回一個描述符(id),因此你可以使用cancelAnimationFrame取消RAF回調(diào)(就像使用setTimeout一樣);
- 更改元素大小或讀取元素屬性的JS代碼會強制使用requestAnimationFrame;
樣式重新計算(Recalc Style)
圖片
瀏覽器重新計算應(yīng)用的樣式。此步驟還會計算哪些媒體查詢將處于活動狀態(tài)。
以下操作能觸發(fā)重新計算包括
- 直接更改,比如 a.styles.left = '10px'
- 通過CSS文件描述的更改,比如 element.classList.add('my-styles-class')。
此過程可能觸發(fā)整個DOM樹的整體計算也可以是局部小范圍的計算過程,取決于「被改動的元素的位置」。
- 例如,改動body元素的屬性,就會發(fā)生整個DOM樹的重新計算。
圖片
將元素樣式和DOM元素結(jié)合起來,就會生成Render Tree
我們可以通過devTool-Performance來檢測網(wǎng)站在這步所花費的時間。
圖片
React官網(wǎng)-樣式重新計算所花費時間
布局(Layout)
計算每個「可視元素」的位置信息(距離視口的距離和元素本身大小)。并生成對應(yīng)的Layout Tree。 頁面上的DOM元素越多,操作就越復(fù)雜。
圖片
React官網(wǎng)-布局所花費時間
觸發(fā)條件
對于現(xiàn)今「富應(yīng)用」來講,做頁面中做元素的移動和變更那是家常便飯。以下操作就會觸發(fā)對應(yīng)的布局流程
- 讀取與元素的大小和位置相關(guān)的屬性(offsetWidth、offsetLeft、getBoundingClientRect等)。
- 寫入與元素的大小和位置相關(guān)的屬性,除了某些屬性(例如 transform 和 will-change)。
這里,我們用一點篇幅來講一下為何transform/will-change能跳過布局階段,直接進入合成階段。
在之前的Chromium 最新渲染引擎--RenderingNG我們講過,在渲染流程的最開始其實是還有一個步驟叫做animate。
圖片
在渲染流程的圖中,用不同顏色來標識該階段可能會被不同的線程或者進程所執(zhí)行。
顏色 | 所在進程/線程 |
綠色 | 渲染進程中的主線程 |
黃色 | 渲染進程中的合成線程 |
橘色 | viz進程(也叫GPU進程) |
在某些階段,可能會被多個地方所執(zhí)行,所以該階段可能存在多個顏色。
而animate存在兩個顏色(綠色和黃色)也就是這一步,會在多個地方被執(zhí)行。
而讓animate能夠在黃色階段,也就是在合成線程中執(zhí)行,就是我們使用了一些CSS3屬性
- transform
- opacity
- filter
- will-change
使用了這些屬性后,會跳過前面很多的步驟,例如重新計算樣式/布局/重繪等階段。并且,在合成線程進行數(shù)據(jù)轉(zhuǎn)換后,開啟GPU的「硬件加速」。
就這速度,你說能不快嗎。
其實,上面的內(nèi)容在大部分教程中,都是一種鐵打不動的定律。使用了transform/will-change開啟了GPU硬件加速,所以性能提升了。
強制布局
強制布局(Forced Synchronous Layout 或 Forced Reflow)是Web性能優(yōu)化領(lǐng)域的一個術(shù)語,它指的是瀏覽器在能夠繼續(xù)「處理后續(xù)操作之前,必須完成當前的布局計算」。
當強制執(zhí)行布局時,瀏覽器會暫停JS主線程,盡管調(diào)用棧不是空的。
有很多我們耳熟能詳?shù)牟僮鳎紩|發(fā)強制布局。
圖片
想了解更多??觸發(fā)強制布局的操作[4]。
案例分析
div1.style.height = "200px"; // 更改元素大小
var height1 = div1.clientHeight; // 讀取屬性
瀏覽器無法在不重新計算其實際大小的情況下計算div1的clientHeight。在這種情況下,瀏覽器暫停JS執(zhí)行并運行:樣式以查看應(yīng)該更改什么,以及布局以重新計算大小。布局不僅計算放置在div1之前的元素,還計算放置在div1之后的元素。現(xiàn)代瀏覽器通過優(yōu)化計算,使得在每次都不必重新計算整個DOM樹,但在糟糕的情況下仍然會發(fā)生。重新計算的過程稱為「layout shift」。
圖片
瀏覽器盡量不在每次都強制執(zhí)行布局。因此,它們對操作進行分組:
div1.style.height = "200px";
var height1 = div1.clientHeight; // <-- 布局 1
div2.style.margin = "300px";
var height2 = div2.clientHeight; // <-- 布局 2
- 在第一行上,瀏覽器計劃了高度的更改。
- 在第二行上,瀏覽器收到了讀取屬性的請求。由于我們有掛起的高度更改,瀏覽器必須強制執(zhí)行布局。
- 在第三 + 四行上我們有相同的情況。為了讓瀏覽器更好地處理,我們可以組合讀取和寫入操作:
div1.style.height = "200px";
div2.style.margin = "300px";
var height1 = div1.clientHeight; // <-- 布局 1
var height2 = div2.clientHeight;
通過組合元素,我們擺脫了第二次布局,因為當瀏覽器達到第四行時,它已經(jīng)有了所有的數(shù)據(jù)。
我們的事件循環(huán)從一個循環(huán)變成了多個,因為我們可以在任務(wù)和微任務(wù)階段都強制執(zhí)行布局:
圖片
優(yōu)化布局
- 將讀取/寫入操作分組,擺脫不必要的布局
- 優(yōu)先采用硬件加速
- 批量DOM更改:集中進行DOM更改,然后一次性讀取布局信息,可以減少強制布局的次數(shù)。
- 避免布局抖動:布局抖動是指在一系列操作中交替進行讀寫操作,導(dǎo)致多次布局計算。通過避免布局抖動,可以改善性能。
- 使用現(xiàn)代CSS布局技術(shù):如Flexbox和Grid,這些技術(shù)可以提高布局性能,尤其是在動態(tài)內(nèi)容變化時。
- 使用虛擬化:如在長列表中,只渲染進入視口的元素,可以極大地減少布局計算的負擔(dān)。
繪制(Paint)
圖片
在Layout階段已經(jīng)把可見元素都安排的明明白白了,現(xiàn)在我們就需要對其粉飾一番了。也就是對其濃墨重彩的處理下。
圖片
這個操作通常不會消耗很多時間,可能在第一次渲染時可能會很大。
合成(Composition)
圖片
合成是默認在GPU上運行的唯一階段。
圖片
在這一步中,瀏覽器僅執(zhí)行特定的CSS樣式,比如transform。(硬件加速那塊介紹過了)
重要說明:transform: translate 不會在GPU上啟動渲染。因此,如果我們在代碼中有 transform: translateZ(0) 來將渲染移到GPU上,這樣是行不通的。這是一個誤解。
我們可以通過源碼找到原因
圖片
在devTool可以在GPU的信息中看到所消耗的時間
圖片
5. EventLoop模型
通過上面的對各個節(jié)點的分析,最終EventLoop的模型圖如下所示:
圖片
偽代碼表示EventLoop
// 開始一個無限循環(huán),模擬瀏覽器的事件循環(huán)
while (true) {
// 記錄任務(wù)開始的時間
const taskStartTime = performance.now();
// 從事件隊列中取出一個任務(wù)
const task = eventQueue.pop();
// 如果任務(wù)存在,則運行該任務(wù)
if (task)
task.run();
// 如果任務(wù)運行時間超過50ms,則報告長任務(wù)
if (performance.now() - taskStartTime > 50)
reportLongTask();
// 如果沒有渲染機會(可能是因為當前處于忙碌狀態(tài)或頻率限制),則繼續(xù)下一次循環(huán)
if (!hasRenderingOpportunity())
continue;
// 調(diào)用所有排隊的動畫幀回調(diào)
invokeAnimationFrameCallbacks();
// 如果需要樣式計算和布局,則執(zhí)行它們
while (needsStyleAndLayout()) {
// 執(zhí)行樣式計算和布局
styleAndLayout();
// 調(diào)用所有的尺寸調(diào)整觀察者回調(diào)
invokeResizeObservers();
}
// 標記繪制時間,這可能是用于性能監(jiān)控
markPaintTiming();
// 執(zhí)行渲染,更新屏幕上的內(nèi)容
render();
}
長任務(wù) 和 50ms
上面的偽代碼中,我們有兩個針對于本文來講比較陌生的詞或者變量
- 長任務(wù)
- 50ms
其實,我們之前在介紹瀏覽器性能指標時有過接觸的。下面我就把這些羅列一下,不做過多的解釋了。
我們在瀏覽器之性能指標-TBT中介紹過
任何超過50毫秒的任務(wù)被認為是「長任務(wù)」
在瀏覽器之性能指標-TTI中的標準中也提到過50ms
圖片
在瀏覽器之性能指標-INP中,我們進行INP優(yōu)化時也涉及長任務(wù)的優(yōu)化處理