瀏覽器層面優化前端效能(1):Chrom元件與程序/執行緒模型分析

2023-04-09 18:00:22

現階段的瀏覽器執行在一個單使用者,多合作,多工的作業系統中。一個糟糕的網頁同樣可以讓一個現代的瀏覽器崩潰。其原因可能是一個外掛出現bug,最終的結果是整個瀏覽器以及其他正在執行的標籤被銷燬。

現代作業系統已經非常健壯了,它讓應用程式在各自的程序中執行和不會影響到其他程式。一個程序崩潰不會損害到其他程序以及作業系統。同時系統會嚴格的限制一個使用者存取另外一個使用者空間的資料。

關於程序、執行緒、多執行緒等相關知識回顧,參看《同步與非同步:並行/並行/程序/執行緒/多cpu/多核/超執行緒/管程

瀏覽器屬於一個應用程式,而應用程式的一次執行,可以理解為計算機啟動了一個程序,程序啟動後,CPU會給該程序分配相應的記憶體空間,當我們的程序得到了記憶體之後,就可以使用執行緒進行資源排程,進而完成我們應用程式的功能。

而在應用程式中,為了滿足功能的需要,啟動的程序會建立另外的新的程序來處理其他任務,這些建立出來的新的程序擁有全新的獨立的記憶體空間,不能與原來的程序內向記憶體,如果這些程序之間需要通訊,可以通過IPC機制(Inter Process Communication)來進行。

IPC機制(Inter Process Communication)

假如我們去開發一個瀏覽器,它的架構可以是一個單程序多執行緒的應用程式,也可以是一個使用IPC通訊的多程序應用程式。

以chrome為例,使用IPC通訊的多程序應用程式

chrome瀏覽器與其他瀏覽器不同,chrome使用多個渲染引擎範例,每個Tab頁一個,即每個Tab都是一個獨立程序。

瀏覽器元件

瀏覽器大體上由以下幾個元件組成,各個瀏覽器可能有一點不同。

瀏覽器元件.png20200610161237613310758.png

  • 介面控制元件 – 包括位址列,前進後退,書籤選單等視窗上除了網頁顯示區域以外的部分

  • 瀏覽器引擎 – 查詢與操作渲染引擎的介面

  • 渲染引擎 – 負責顯示請求的內容。比如請求到HTML, 它會負責解析HTML、CSS並將結果顯示到視窗中

  • 網路 – 用於網路請求, 如HTTP請求。它包括平臺無關的介面和各平臺獨立的實現

  • UI後端 – 繪製基礎元件,如下拉式方塊與視窗。它提供平臺無關的介面,內部使用作業系統的相應實現

  • JS直譯器 - 用於解析執行JavaScript程式碼

  • 資料儲存持久層 - 瀏覽器需要把所有資料存到硬碟上,如cookies。新的HTML5規範規定了一個完整(雖然輕量級)的瀏覽器中的資料庫web database

Chrome的並行模型

chrome的程序,chrome沒有采用一般應用程式的單程序多執行緒的模型,而是採用了多程序的模型,按照他的文字說明,主介面框架下的一個TAB就對應這個一個程序。但實際上,一個程序不僅僅包含一個頁面,實際上同類的頁面在共用一個程序。

Google在宣傳的時候一直都說,Chrome是one tab one process的模式。實際上,Chrome支援的程序模型遠比宣傳豐富,簡單的說,Chrome支援以下幾種程序模型:

  • Process-per-site-instance:就是你開啟一個網站,然後從這個網站鏈開的一系列網站都屬於一個程序。這是Chrome的預設模式。

  • Process-per-site:同域名範疇的網站放在一個程序,比如www.google.com和www.google.com/bookmarks就屬於一個域名內(google有自己的判定機制),不論有沒有互相開啟的關係,都算作是一個程序中。用命令列–process-per-site開啟。

  • Process-per-tab:這個簡單,一個tab一個process,不論各個tab的站點有無聯絡,就和宣傳的那樣。用–process-per-tab開啟。

  • Single Process:這個很熟悉了吧,即傳統瀏覽器的模式:沒有多程序只有多執行緒,用–single-process開啟。

多程序有好處:

把渲染放到另外個程序防止崩潰了影響主程序。webkit最初時候很多記憶體洩露。多程序能很大程度避免。一個程序關了,所有記憶體就回收了。其次,多程序安全性更好。如果blink被發現什麼提權漏洞,例如寫一段js就能控制整個chromium程序做任何事情,顯然多程序可以把損失限制在渲染執行緒。渲染執行緒拿不到主程序的各種私密資訊,例如別的域名下的密碼。

多程序架構的好處

多執行緒模型

chrome程序模型下有

  • Browser程序只有一個,主控整個系統的執行,管理Chrome大部分的日常事務;

    負責瀏覽器頁面的顯示,各個頁面的管理,所有其他型別程序的祖先,負責他們的建立和銷燬。

    • UI thread:

      • 負責瀏覽器介面顯示,與使用者互動。如前進,後退等

      • 將Renderer程序得到的記憶體中的Bitmap,繪製到使用者介面上

    • network thread:網路資源的管理,下載等(這個和網路程序間???)

    • storage thread: 控制檔案等的存取;

  • Renderer 瀏覽器渲染程序(瀏覽器核心),主要負責頁面的渲染和顯示:頁面渲染,指令碼執行,事件處理等

    預設每個Tab頁面一個Renderer程序(Renderer程序,內部是多執行緒的)

  • Utility Network:網路程序,負責頁面網路資源的載入

  • GPU程序:最多隻有一個,GPU硬體加速開啟時才會被建立,用於3D繪製等。

  • NPAPI或PPAPI外掛程序,每種型別的外掛對應一個程序,僅當使用該外掛時才建立。

    1995 年 Netscape 發明了NPAPI (Netscape plugin API)這個種架構,來幫助瀏覽器渲染一些HTML沒有的東西。比如 PDF, 比如 視訊, 以及等等。NPAPI不限制外掛自由存取系統所有的API,而且和瀏覽器是平級執行的。現在已被禁用。 PPAPI是谷歌提出的架構。

  • Pepper外掛程序

  • 其他型別的程序,比如Linux的Zygote程序;Sandbox程序。

chrome程序架構圖瀏覽器渲染程序通訊

Browser作為主程序最先啟動,Browser包含一個主執行緒mainthread,在mainthread中對整個系統進行初始化,並啟動為另外幾個執行緒,看下面的程式碼:

void CreateChildThreads(BrowserProcessImpl* process) {

  process->db_thread(); //負責資料庫處理

  process->file_thread(); // 負責檔案管理

  process->process_launcher_thread();

  process->cache_thread(); //負責管理快取

  process->io_thread(); //負責管理程序間通訊和所有I/O行為。

}

io_thread不僅負責Browser程序的I/O,而且其他Renderer的I/O請求也會通過程序間通訊傳送到這個執行緒,由該執行緒進行處理,最後把結果在返回給各個Renderer程序。各個執行緒的功能不一樣,但設計模式是一樣的

 

對於Renderer程序,它們通常有兩個執行緒:一個是Main thread,負責與主執行緒聯絡。另一個是Render thread,它們負責頁面的渲染和互動

20200610161318112145652.png

當我們是要瀏覽一個網頁,我們會在瀏覽器的位址列裡輸入URL,這個時候Browser Process會向這個URL傳送請求,獲取這個URL的HTML內容,然後將HTML交給Renderer Process,Renderer Process解析HTML內容,解析遇到需要請求網路的資源又返回來交給Browser Process進行載入,同時通知Browser Process,需要Plugin Process載入外掛資源,執行外掛程式碼。解析完成後,Renderer Process計算得到影象幀,並將這些影象幀交給GPU Process,GPU Process將其轉化為影象顯示螢幕。

 

Chrome的執行緒模型

Chrome的執行緒模型極力規避鎖的存在,將鎖限制了極小的範圍內(僅僅在將Task放入訊息佇列的時候才存在…),並且使得上層完全不需要關心鎖的問題(當然,前提是遵循它的程式設計模型,將函數用Task封裝並行送到合適的執行緒去執行…),大大簡化了開發的邏輯。它用到了訊息迴圈的手段。每一個Chrome的執行緒,入口函數都差不多,都是啟動一個訊息迴圈(參見MessagePump類),等待並執行任務。

根據執行緒處理事務類別的不同,所起的訊息迴圈有所不同。比如

  • 處理程序間通訊的執行緒(注意,在Chrome中,這類執行緒都叫做IO執行緒)

  • 啟用的是MessagePumpForIO類,處理UI的執行緒用的是MessagePumpForUI類,

  • 一般的執行緒用到的是MessagePumpDefault類(只討論windows)。

不同的訊息迴圈類,主要差異有兩個,一是訊息迴圈中需要處理什麼樣的訊息和任務,第二個是迴圈流程(比如是死迴圈還是阻塞在某號誌上…)。

瀏覽器通常由以下常駐執行緒組成:

  • GUI 渲染執行緒
    GUI渲染執行緒負責渲染瀏覽器介面HTML元素,當介面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該執行緒就會執行。在Javascript引擎執行指令碼期間,GUI渲染執行緒都是處於掛起狀態的,也就是說被凍結了.

    • 一個主執行緒(main thread)

    • 多個工作執行緒(work thread)

    • 一個合成器執行緒(compositor thread)

    • 多個光柵化執行緒(raster thread)

  • JavaScript引擎執行緒
    JS為處理頁面中使用者的互動,以及操作DOM樹、CSS樣式樹來給使用者呈現一份動態而豐富的互動體驗和伺服器邏輯的互動處理。如果JS是多執行緒的方式來操作這些UI DOM,則可能出現UI操作的衝突;如果JS是多執行緒的話,在多執行緒的互動下,處於UI中的DOM節點就可能成為一個臨界資源,假設存在兩個執行緒同時操作一個DOM,一個負責修改一個負責刪除,那麼這個時候就需要瀏覽器來裁決如何生效哪個執行緒的執行結果,當然我們可以通過鎖來解決上面的問題。但為了避免因為引入了鎖而帶來更大的複雜性(多執行緒的話會使瀏覽器的效率降低。多執行緒必然會引入的鎖,號誌的一類操作,大大增加了複雜性),JS在最初就選擇了單執行緒執行
    GUI渲染執行緒與JS引擎執行緒互斥的,是由於JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染介面(即JavaScript執行緒和UI執行緒同時執行),那麼渲染執行緒前後獲得的元素資料就可能不一致。當JavaScript引擎執行時GUI執行緒會被掛起,GUI更新會被儲存在一個佇列中等到引擎執行緒空閒時立即被執行。
    由於GUI渲染執行緒與JS執行執行緒是互斥的關係,當瀏覽器在執行JS程式的時候,GUI渲染執行緒會被儲存在一個佇列中,直到JS程式執行完成,才會接著執行。因此如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞的感覺。

  • 定時觸發器執行緒
    瀏覽器定時計數器並不是由JS引擎計數的, 因為JS引擎是單執行緒的, 如果處於阻塞執行緒狀態就會影響記計時的準確, 因此通過單獨執行緒來計時並觸發定時是更為合理的方案。

  • 事件觸發執行緒
    當一個事件被觸發時該執行緒會把事件新增到待處理佇列的隊尾,等待JS引擎的處理。這些事件可以是當前執行的程式碼塊如定時任務、也可來自瀏覽器核心的其他執行緒如滑鼠點選、AJAX非同步請求等,但由於JS的單執行緒關係所有這些事件都得排隊等待JS引擎處理。

  • 非同步http請求執行緒
    在XMLHttpRequest在連線後是通過瀏覽器新開一個執行緒請求,將檢測到狀態變更時,如果設定有回撥函數,非同步執行緒就產生狀態變更事件放到JS引擎的處理佇列中等待處理。

對於普通的前端操作來說,最終要的是什麼呢?答案是渲染程序

可以這樣理解,頁面的渲染,JS的執行,事件的迴圈,都在這個程序內進行。

Browser Process程序:

  • tab以外的大部分工作由瀏覽器程序Browser Process負責,Browser Process 劃分出不同的工作執行緒

  • UI thread:控制瀏覽器上的按鈕及輸入框;

  • network thread:處理網路請求,從網上獲取資料(Chrome72以後,已將network thread單獨摘成network service process,當然也可以通過 chrome://flags/#network-service-in-process修改設定,將其其作為執行緒執行在Browser Process中,感謝 @Popeye-Wz 的提出);

  • storage thread: 控制檔案等的存取;

Browser Process 劃分出不同的工作執行緒

網頁載入過程-導航過程

  • UI thread:控制瀏覽器上的按鈕及輸入框;

  • network thread:處理網路請求,從網上獲取資料(Chrome72以後,已將network thread單獨摘成network service process,當然也可以通過 chrome://flags/#network-service-in-process修改設定,將其其作為執行緒執行在Browser Process中

  • storage thread: 控制檔案等的存取;

處理過程解析

處理輸入

當我們在瀏覽器的位址列輸入內容按下回車時,UI thread會判斷輸入的內容是搜尋關鍵詞(search query)還是URL,如果是搜尋關鍵詞,跳轉至預設搜尋引擎對應都搜尋URL,如果輸入的內容是URL,則開始請求URL。

開始導航

回車按下後,UI thread將關鍵詞搜尋對應的URL或輸入的URL交給網路執行緒Network thread,此時UI執行緒使Tab前的圖示展示為載入中狀態,然後網路程序進行一系列諸如DNS定址,建立TLS連線等操作進行資源請求,如果收到伺服器的301重定向響應,它就會告知UI執行緒進行重定向然後它會再次發起一個新的網路請求。

讀取響應

network thread接收到伺服器的響應後,開始解析HTTP響應報文,然後根據響應頭中的Content-Type欄位來確定響應主體的媒體型別(MIME Type),如果媒體型別是一個HTML檔案,則將響應資料交給渲染程序(renderer process)來進行下一步的工作,如果是 zip 檔案或者其它檔案,會把相關資料傳輸給下載管理器。

與此同時,瀏覽器會進行 Safe Browsing 安全檢查,如果域名或者請求內容匹配到已知的惡意站點,network thread 會展示一個警告頁。除此之外,網路執行緒還會做 CORB(Cross Origin Read Blocking)檢查來確定那些敏感的跨站資料不會被傳送至渲染程序。

查詢渲染程序

各種檢查完畢以後,network thread 確信瀏覽器可以導航到請求網頁,network thread 會通知 UI thread 資料已經準備好,UI thread 會查詢到一個 renderer process 進行網頁的渲染。

瀏覽器為了對查詢渲染程序這一步驟進行優化,考慮到網路請求獲取響應需要時間,所以在第二步開始,瀏覽器已經預先查詢和啟動了一個渲染程序,如果中間步驟一切順利,當 network thread 接收到資料時,渲染程序已經準備好了,但是如果遇到重定向,這個準備好的渲染程序也許就不可用了,這個時候會重新啟動一個渲染程序。

提交導航

到了這一步,資料和渲染程序都準備好了,Browser Process 會向 Renderer Process 傳送IPC訊息來確認導航,此時,瀏覽器程序將準備好的資料傳送給渲染程序,渲染程序接收到資料之後,又傳送IPC訊息給瀏覽器程序,告訴瀏覽器程序導航已經提交了,頁面開始載入。

這個時候導航欄會更新,安全指示符更新(地址前面的小鎖),存取歷史列表(history tab)更新,即可以通過前進後退來切換該頁面。

初始化載入完成

當導航提交完成後,渲染程序開始載入資源及渲染頁面(詳細內容下文介紹),當頁面渲染完成後(頁面及內部的iframe都觸發了onload事件),會向瀏覽器程序傳送IPC訊息,告知瀏覽器程序,這個時候UI thread會停止展示tab中的載入中圖示。

渲染程序

    1. GUI渲染執行緒

      • 負責渲染瀏覽器介面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。

      • 當介面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該執行緒就會執行

      • 注意,GUI渲染執行緒與JS引擎執行緒是互斥的,當JS引擎執行時GUI執行緒會被掛起(相當於被凍結了),GUI更新會被儲存在一個佇列中等到JS引擎空閒時立即被執行。

        • 由於JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染介面(即JS執行緒和UI執行緒同時執行),那麼渲染執行緒前後獲得的元素資料就可能不一致了。因此為了防止渲染出現不可預期的結果,瀏覽器設定GUI渲染執行緒與JS引擎為互斥的關係,當JS引擎執行時GUI執行緒會被掛起,GUI更新則會被儲存在一個佇列中等到JS引擎執行緒空閒時立即被執行。

      • 注意composite概念,瀏覽器渲染的圖層一般包含兩大類:普通圖層以及複合圖層。

        • 普通檔案流內可以理解為一個複合圖層(這裡稱為預設複合層,裡面不管新增多少元素,其實都是在同一個複合圖層中,哪怕是absolute佈局(fixed也一樣),即使脫離普通檔案流,但它仍然屬於預設複合層)

          • absolute雖然可以脫離普通檔案流,但是無法脫離預設複合層。就算absolute中資訊改變時不會改變普通檔案流中render樹,但是,瀏覽器最終繪製時,是整個複合層繪製的,所以absolute中資訊的改變,仍然會影響整個複合層的繪製。

        • 可以通過硬體加速的方式—GPU執行緒,宣告一個新的複合圖層(最常用的方式:translate3d、translateZ),它會單獨分配資源,會脫離普通檔案流,不管這個複合圖層中怎麼變化,也不會影響預設複合層裡的迴流重繪。

          • GPU中,各個複合圖層是單獨繪製的,所以互不影響,這也是為什麼某些場景硬體加速效果一級棒

          • 如果a是一個複合圖層,而且b在a上面,那麼b也會被隱式轉為一個複合圖層,這點需要特別注意

      • css載入是否會阻塞dom 渲染程序

        • css載入不會阻塞DOM樹解析(非同步載入時DOM照常構建——css是由單獨的下載執行緒非同步下載的)

        • 但會阻塞render樹渲染(渲染時需等css載入完畢,因為render樹需要css資訊——這可能也是瀏覽器的一種優化機制)

因為載入css的時候,可能會修改下面DOM節點的樣式,如果css載入不阻塞render樹渲染的話,那麼當css載入完之後,render樹可能又得重新重繪或者回流了,這就造成了一些沒有必要的損耗。所以乾脆就先把DOM樹的結構先解析完,把可以做的工作做完,然後等你css載入完之後,在根據最終的樣式來渲染render樹,這種做法效能方面確實會比較好一點。

  1. JS引擎執行緒

    • 也稱為JS核心,負責處理Javascript指令碼程式。(例如V8引擎)

    • JS引擎執行緒負責解析Javascript指令碼,執行程式碼。

    • JS引擎一直等待著任務佇列中任務的到來,然後加以處理,一個Tab頁(renderer程序)中無論什麼時候都只有一個JS執行緒在執行JS程式

    • 同樣注意,GUI渲染執行緒與JS引擎執行緒是互斥的,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞。

    • 要儘量避免JS執行時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞的感覺。Web Worker  非同步優化下

      • 建立Worker時,JS引擎向瀏覽器申請開一個子執行緒(子執行緒是瀏覽器開的,完全受主執行緒控制,而且不能操作DOM)

      • JS引擎執行緒與worker執行緒間通過特定的方式通訊(postMessage API,需要通過序列化物件來與執行緒互動特定的資料)

        JS引擎是單執行緒的,這一點的本質仍然未改變,Worker可以理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題。

    • SharedWorker是瀏覽器所有頁面共用的,不能採用與Worker同樣的方式實現,因為它不隸屬於某個Render程序,可以為多個Render程序共用使用。所以Chrome瀏覽器為SharedWorker單獨建立一個程序來執行JavaScript程式,在瀏覽器中每個相同的JavaScript只存在一個SharedWorker程序,不管它被建立多少次。

      • 頁面A傳送資料給worker:window.worker.port.postMessage('get'),然後開啟頁面B,呼叫window.worker.port.postMessage('get'),即可收到頁面A傳送給worker的資料。

  2. 事件觸發執行緒

    • 歸屬於瀏覽器而不是JS引擎,用來控制事件迴圈(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開執行緒協助)

    • 當JS引擎執行程式碼塊如setTimeOut時(也可來自瀏覽器核心的其他執行緒,如滑鼠點選、AJAX非同步請求等),會將對應任務新增到事件執行緒中.

    • 當對應的事件符合觸發條件被觸發時,該執行緒會把事件新增到待處理佇列的隊尾,等待JS引擎的處理

    • 注意,由於JS的單執行緒關係,所以這些待處理佇列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時才會去執行)

      • 為什麼有時候setTimeout推入的事件不能準時執行?因為可能在它推入到事件列表時,主執行緒還不空閒,正在執行其它程式碼,

  3. 定時觸發器執行緒

    • 傳說中的setIntervalsetTimeout所線上程

    • 瀏覽器定時計數器並不是由JavaScript引擎計數的,(因為JavaScript引擎是單執行緒的, 如果處於阻塞執行緒狀態就會影響記計時的準確)

    • 因此通過單獨執行緒來計時並觸發定時(計時完畢後,新增到事件佇列中,等待JS引擎空閒後執行)

    • 注意,W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms。

  4. 非同步http請求執行緒

    • 在XMLHttpRequest在連線後是通過瀏覽器新開一個執行緒請求

    • 將檢測到狀態變更時,如果設定有回撥函數,非同步執行緒就產生狀態變更事件,將這個回撥再放入事件佇列中。再由JavaScript引擎執行。

事件迴圈機制進與執行緒關係

之前也寫過《弄懂javascript的執行機制:事件輪詢|微任務和宏任務》,但是還是沒有從本質去闡述。

JavaScript事件佇列等原因還是JavaScript執行緒與 定時觸發器執行緒、事件觸發執行緒、非同步http請求執行緒等IO通訊問題。《》

  • 主執行緒執行時會產生執行棧

  • 棧中的程式碼呼叫某些api時,它們會在事件佇列中新增各種事件(當滿足觸發條件後,如ajax請求完畢)

  • 而棧中的程式碼執行完畢,就會讀取事件佇列中的事件,去執行那些回撥

如此迴圈,如下圖

20200610173447689929215.png

注意,總是要等待棧中的程式碼執行完畢後才會去讀取事件佇列中的事件

有執行棧與任務佇列,引發,宏任務-macrotask與微任務-microtask等相關概念

在ECMAScript中,microtask稱為jobs,macrotask可稱為task

  • macrotask(又稱之為宏任務),macrotask中的事件都是放在一個事件佇列中的,而這個佇列由事件觸發執行緒維護 

    可以理解是每次執行棧執行的程式碼就是一個宏任務(包括每次從事件佇列中獲取一個事件回撥並放到執行棧中執行),(`task->渲染->task->...`)

     

    • 每一個task會從頭到尾將這個任務執行完畢,不會執行其它

    • 瀏覽器為了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行重新渲染

  • microtask(又稱為微任務),microtask中的所有微任務都是新增到微任務佇列(Job Queues)中,等待當前macrotask執行完畢後執行,而這個佇列由JS引擎執行緒維護

    可以理解是在當前 task 執行結束後立即執行的任務

    • 也就是說,在當前task任務後,下一個task之前,在渲染之前

    • 所以它的響應速度相比setTimeout(setTimeout是task)會更快,因為無需等渲染

    • 也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)

分別很麼樣的場景會形成macrotask和microtask呢?

  • macrotask:主程式碼塊,setTimeout、postMessage、setImmediat、MessageChannel(優先順序高於setTimeout)等(可以看到,事件佇列中的每一個事件都是一個macrotask)、requestAnimationFrame 、I/O、UI Rendering

  • microtask:Promise,MutationObserver(優先順序小於Promise—監聽一個DOM變動),process.nextTick(process.nextTick的優先順序高於Promise) 、Object.observe(廢棄)等

    注意promise的polyfill與官方版本的區別:

    • 官方版本中,是標準的microtask形式

    • polyfill,一般都是通過setTimeout模擬的,所以是macrotask形式

關於vue的$nextTick 2.5+由 MutationObserver 改為MessageChannel,這方面的內容,具體參看《web messaging與Woker分類:漫談postMessage跨執行緒跨頁面通訊

定時器

上述事件迴圈機制的核心是:JS引擎執行緒和事件觸發執行緒

但事件上,裡面還有一些隱藏細節,譬如呼叫setTimeout後,是如何等待特定時間後才新增到事件佇列中的?

是JS引擎檢測的麼?當然不是了。它是由定時器執行緒控制(因為JS引擎自己都忙不過來,根本無暇分身)

為什麼要單獨的定時器執行緒?因為JavaScript引擎是單執行緒的, 如果處於阻塞執行緒狀態就會影響記計時的準確,因此很有必要單獨開一個執行緒用來計時。

什麼時候會用到定時器執行緒?當使用setTimeout或setInterval時,它需要定時器執行緒計時,計時完成後就會將特定的事件推入事件佇列中

setTimeout與setInterval

  • setTimeout計時到到後觸發事件觸發器,插入一個任務到 事件佇列

    延緩事件為:setTimeout觸發是設定的等待事件+等待到任務執行時間)

  • setInterval則是每次都精確的隔一段時間推入一個事件

而且setInterval有一些比較致命的問題就是:累計效應

如果setInterval程式碼在(setInterval)再次新增到佇列之前還沒有完成執行,就會導致定時器程式碼連續執行好幾次,而之間沒有間隔。

JS引擎會對setInterval進行優化,如果當前事件佇列中有setInterval的回撥,不會重複新增。但是,有錯過了延遲的事件。

一般認為的最佳方案是:用setTimeout模擬setInterval,或者特殊場合直接用requestAnimationFrame

 

Node.js事件迴圈與執行緒

Node.js也是單執行緒的Event Loop,但是它的執行機制不同於瀏覽器(和瀏覽器中的是完全不相同的東西,關鍵還是執行緒架構不同)

Node.js 採用 V8 作為 js 的解析引擎,而 I/O 處理方面使用了自己設計的 libuv,libuv 是一個基於事件驅動的跨平臺抽象層,封裝了不同作業系統一些底層特性,對外提供統一的 API,事件迴圈機制也是它裡面的實現

Node.js也是單執行緒的Event Loop

根據上圖,Node.js的執行機制如下

  • V8引擎解析JavaScript指令碼

  • 解析後的程式碼,呼叫Node API

  • libuv庫負責Node API的執行。它將不同的任務分配給不同的執行緒,形成一個Event Loop(事件迴圈),以非同步的方式將任務的執行結果返回給V8引擎

  • V8引擎再將結果返回給使用者

Node.js 的執行機制

  • V8 引擎解析 JavaScript 指令碼。

  • 解析後的程式碼,呼叫 Node API。

  • libuv 庫負責 Node API 的執行。它將不同的任務分配給不同的執行緒,形成一個 Event Loop(事件迴圈),以非同步的方式將任務的執行結果返回給 V8 引擎。

  • V8 引擎再將結果返回給使用者。

libuv 引擎中的事件迴圈6個階段

libuv 引擎中的事件迴圈分為 6 個階段,它們會按照順序反覆執行。每當進入某一個階段的時候,都會從對應的回撥佇列中取出函數去執行。當佇列為空或者執行的回撥函數數量到達系統設定的閾值,就會進入下一階段。

libuv 引擎中的事件迴圈分為 6 個階段

從上圖中,大致看出 node 中的事件迴圈的順序:

 

外部輸入資料–>輪詢階段(poll)–>檢查階段(check)–>關閉事件回撥階段(close callback)–>定時器檢測階段(timer)–>I/O 事件回撥階段(I/O callbacks)–>閒置階段(idle, prepare)–>輪詢階段(按照該順序反覆執行)…

    • timers 階段:這個階段執行 timer(setTimeout、setInterval)的回撥

      • timers 階段會執行 setTimeout 和 setInterval 回撥,並且是由 poll 階段控制的。

      • 同樣,在 Node 中定時器指定的時間也不是準確時間,只能是儘快執行。

    • I/O callbacks 階段:處理一些上一輪迴圈中的少數未執行的 I/O 回撥

    • idle, prepare 階段:僅 node 內部使用

    • poll 階段:獲取新的 I/O 事件, 適當的條件下 node 將阻塞在這裡

      poll 是一個至關重要的階段,這一階段中,系統會做兩件事情

      • 回到 timer 階段執行回撥

      • 執行 I/O 回撥

並且在進入該階段時如果沒有設定了 timer 的話,會發生以下兩件事情

    • 如果 poll 佇列不為空,會遍歷回撥佇列並同步執行,直到佇列為空或者達到系統限制

    • 如果 poll 佇列為空時,會有兩件事發生

      • 如果有 setImmediate 回撥需要執行,poll 階段會停止並且進入到 check 階段執行回撥

      • 如果沒有 setImmediate 回撥需要執行,會等待回撥被加入到佇列中並立即執行回撥,這裡同樣會有個超時時間設定防止一直等待下去

  • check 階段:執行 setImmediate() 的回撥

    setImmediate()的回撥會被加入 check 佇列中,從 event loop 的階段圖可以知道,check 階段的執行順序在 poll 階段之後。

    • setImmediate 設計在 poll 階段完成時執行,即 check 階段;

    • setTimeout 設計在 poll 階段為空閒時,且設定時間到達後執行,但它在 timer 階段執行

  • close callbacks 階段:執行 socket 的 close 事件回撥

注意:上面六個階段都不包括 process.nextTick()

process.nextTick 這個函數其實是獨立於 Event Loop 之外的,它有一個自己的佇列,當每個階段完成後,如果存在 nextTick 佇列,就會清空佇列中的所有回撥函數,並且優先於其他 microtask 執行。

瀏覽器環境下,microtask 的任務佇列是每個 macrotask 執行完之後執行。而在 Node.js 中,microtask 會在事件迴圈的各個階段之間執行,也就是一個階段執行完畢,就會去執行 microtask 佇列的任務。

每個階段都有一個先進先出的回撥函數佇列。只有一個階段的回撥函數佇列清空了,該執行的回撥函數都執行了,事件迴圈才會進入下一個階段。

Node 與瀏覽器的 Event Loop 差異

nodejs 寫的少,沒有過多深入

function f () {
  console.log('start')
  setTimeout(() => {
    console.log('timer1')
    Promise.resolve().then(function() {
      console.log('promise1')
    })
  }, 0)
  setTimeout(() => {
    console.log('timer2')
    Promise.resolve().then(function() {
      console.log('promise2')
    })
  }, 0)
  Promise.resolve().then(function() {
    console.log('promise3')
  })

  console.log('end')

}
f()

現在,瀏覽器和node12,輸出順序是一樣的。推薦閱讀軟老師的《Node 定時器詳解

 

從文章的 瀏覽器通常由以下常駐執行緒組成 裡面的 渲染程序  已知,GUI渲染執行緒與JS引擎執行緒是互斥的,他們會阻塞頁面渲染。所以我們從瀏覽器的去分析下,怎麼優化前端的效能呢?

下篇《瀏覽器層面優化前端效能(2):Reader引擎執行緒與模組分析優化點

 

參考文章:

前端都該懂的瀏覽器工作原理,你懂了嗎? https://segmentfault.com/a/1190000022633988

從瀏覽器多程序到JS單執行緒,JS執行機制最全面的一次梳理 https://www.cnblogs.com/cangqinglang/p/8963557.html

Chrome原始碼剖析、上--多執行緒模型、程序通訊、程序模型https://www.cnblogs.com/v-July-v/archive/2011/04/02/2036008.html

Chrome原始碼分析之程序和執行緒模型(三) https://blog.csdn.net/namelcx/article/details/6582730

http://dev.chromium.org/developers/design-documents/multi-process-architecture

chrome渲染機制淺析 https://www.jianshu.com/p/99e450fc04a5

淺析瀏覽器渲染原理 https://segmentfault.com/a/1190000012960187

javascript宏任務和微任務 https://www.cnblogs.com/fangdongdemao/p/10262209.html

瀏覽器與Node的事件迴圈(Event Loop)有何區別? https://blog.csdn.net/Fundebug/article/details/86487117

 


轉載本站文章《瀏覽器層面優化前端效能(1):Chrom元件與程序/執行緒模型分析》,
請註明出處:https://www.zhoulujun.cn/html/webfront/browser/webkit/2020_0610_8455.html