淺談瀏覽器多進程與JS線程

引言

一直對瀏覽器的進程、線程的運行一無所知,經過一次的刷刷刷相關的博客之後,對其有了初步的瞭解,是時候該總結一波了。

進程、線程之間的關係

一個進程有一個或多個線程,線程之間共同完成進程分配下來的任務。打個比方:

  • 假如進程是一個工廠,工廠有它的獨立的資源
  • 工廠之間相互獨立
  • 線程是工廠中的工人,多個工人協作完成任務
  • 工廠內有一個或多個工人
  • 工人之間共享空間

再完善完善概念:

  • 工廠的資源 -> 系統分配的內存(獨立的一塊內存)
  • 工廠之間的相互獨立 -> 進程之間相互獨立
  • 多個工人協作完成任務 -> 多個線程在進程中協作完成任務
  • 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成
  • 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)

進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位),線程是cpu調度的最小單位(線程是建立在進程的基礎上的一次程序運行單位)。

瀏覽器內的進程

知道了進程與線程之間的關係之後,下面是瀏覽器與進程的關係了。首先,瀏覽器是多進程的,之所以瀏覽器能夠運行,是因爲系統給瀏覽器分配了資源,如cpu、內存,簡單的說就是,瀏覽器每打開一個標籤頁,就相當於創建了一個獨立的瀏覽器進程。例如我們查看chrome裏面的任務管理器。

注意: 在這裏瀏覽器應該也有自己的優化機制,有時候打開多個tab頁後,可以在Chrome任務管理器中看到,有些進程被合併了(譬如打開多個空白標籤頁後,會發現多個空白標籤頁被合併成了一個進程),所以每一個Tab標籤對應一個進程並不一定是絕對的。

除了瀏覽器的標籤頁進程之外,瀏覽器還有一些其他進程來輔助支撐標籤頁的進程,如下:
① Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。作用有

  • 負責瀏覽器界面顯示,與用戶交互。如前進,後退等
  • 負責各個頁面的管理,創建和銷燬其他進程
  • 網絡資源的管理,下載等

② 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
③ GPU進程:最多一個,用於3D繪製等
④ 瀏覽器渲染進程(瀏覽器內核),Renderer進程,內部是多線程的,也就是我們每個標籤頁所擁有的進程,互不影響,負責頁面渲染,腳本執行,事件處理等

如下圖:

圖片描述

瀏覽器內核

瀏覽器內核,即我們的渲染進程,有名Renderer進程,我們頁面的渲染,js的執行,事件的循環都在這一進程內進行,也就是說,該進程下面擁有着多個線程,靠着這些現成共同完成渲染任務。那麼這些線程是什麼呢,如下:

① 圖形用戶界面GUI渲染線程

  • 負責渲染瀏覽器界面,包括解析HTML、CSS、構建DOM樹、Render樹、佈局與繪製等
  • 當界面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該線程就會執行

② JS引擎線程

  • JS內核,也稱JS引擎,負責處理執行javascript腳本
  • 等待任務隊列的任務的到來,然後加以處理,瀏覽器無論什麼時候都只有一個JS引擎在運行JS程序

③ 事件觸發線程

  • 聽起來像JS的執行,但是其實歸屬於瀏覽器,而不是JS引擎,用來控制時間循環(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協助)
  • 當JS引擎執行代碼塊如setTimeout時(也可來自瀏覽器內核的其他線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件線程中
  • 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理
  • 注意:由於JS的單線程關係,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)

④ 定時觸發器線程

  • setIntervalsetTimeout所在線程
  • 定時計時器並不是由JS引擎計時的,因爲如果JS引擎是單線程的,如果JS引擎處於堵塞狀態,那會影響到計時的準確
  • 當計時完成被觸發,事件會被添加到事件隊列,等待JS引擎空閒了執行
  • 注意:W3C的HTML標準中規定,setTimeout中低與4ms的時間間隔算爲4ms

⑤ 異步HTTP請求線程

  • 在XMLHttpRequest在連接後新啓動的一個線程
  • 線程如果檢測到請求的狀態變更,如果設置有回調函數,該線程會把回調函數添加到事件隊列,同理,等待JS引擎空閒了執行

瀏覽器內核,放圖加強記憶:

圖片描述

爲什麼JS引擎是單線程的

JavaScript作爲一門客戶端的腳本語言,主要的任務是處理用戶的交互,而用戶的交互無非就是響應DOM的增刪改,使用事件隊列的形式,一次事件循環只處理一個事件響應,使得腳本執行相對連續。如果JS引擎被設計爲多線程的,那麼DOM之間必然會存在資源競爭,那麼語言的實現會變得非常臃腫,在客戶端跑起來,資源的消耗和性能將會是不太樂觀的,故設計爲單線程的形式,並附加一些其他的線程來實現異步的形式,這樣運行成本相對於使用JS多線程來說降低了很多。

瀏覽器內核中線程之間的關係

GUI渲染線程與JS引擎線程互斥

因爲JS引擎可以修改DOM樹,那麼如果JS引擎在執行修改了DOM結構的同時,GUI線程也在渲染頁面,那麼這樣就會導致渲染線程獲取的DOM的元素信息可能與JS引擎操作DOM後的結果不一致。爲了防止這種現象,GUI線程與JS線程需要設計爲互斥關係,當JS引擎執行的時候,GUI線程需要被凍結,但是GUI的渲染會被保存在一個隊列當中,等待JS引擎空閒的時候執行渲染。
由此也可以推出,如果JS引擎正在進行CPU密集型計算,那麼JS引擎將會阻塞,長時間不空閒,導致渲染進程一直不能執行渲染,頁面就會看起來卡頓卡頓的,渲染不連貫,所以,要儘量避免JS執行時間過長。

JS引擎線程與事件觸發線程、定時觸發器線程、異步HTTP請求線程

事件觸發線程、定時觸發器線程、異步HTTP請求線程三個線程有一個共同點,那就是使用回調函數的形式,當滿足了特定的條件,這些回調函數會被執行。這些回調函數被瀏覽器內核理解成事件,在瀏覽器內核中擁有一個事件隊列,這三個線程當滿足了內部特定的條件,會將這些回調函數添加到事件隊列中,等待JS引擎空閒執行。例如異步HTTP請求線程,線程如果檢測到請求的狀態變更,如果設置有回調函數,回調函數會被添加事件隊列中,等待JS引擎空閒了執行。
但是,JS引擎對事件隊列(宏任務)與JS引擎內的任務(微任務)執行存在着先後循序,當每執行完一個事件隊列的時間,JS引擎會檢測內部是否有未執行的任務,如果有,將會優先執行(微任務)。

WebWorker

因爲JS引擎是單線程的,當JS執行時間過長會頁面阻塞,那麼JS就真的對CPU密集型計算無能爲力麼?

所以,後來HTML5中支持了 Web Worker

來自MDN的官方解釋

Web Workers 使得一個Web應用程序可以在與主執行線程分離的後臺線程中運行一個腳本操作。這樣做的好處是可以在一個單獨的線程中執行費時的處理任務,從而允許主(通常是UI)線程運行而不被阻塞/放慢。

注意點:

  • WebWorker可以想瀏覽器申請一個子線程,該子線程服務於主線程,完全受主線程控制。
  • JS引擎線程與worker線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數據)

所以,如果需要進行一些高耗時的計算時,可以單獨開啓一個WebWorker線程,這樣不管這個WebWorker子線程怎麼密集計算、怎麼阻塞,都不會影響JS引擎主線程,只需要等計算結束,將結果通過postMessage傳輸給主線程就可以了。

另外,還有個東西叫 SharedWorker,與WebWorker在概念上所不同。

  • WebWorker 只屬於某一個頁面,不會和其他標籤頁的Renderer進程共享,WebWorker是屬於Renderer進程創建的進程。
  • SharedWorker 是由瀏覽器單獨創建的進程來運行的JS程序,它被所有的Renderer進程所共享,在瀏覽器中,最多隻能存在一個SharedWorker進程。

SharedWorker由進程管理,WebWorker是某一個Renderer進程下的線程。

瀏覽器的渲染流程

每個瀏覽器內核的渲染流程不一樣,下面我們主要以webkit爲主。
首先是渲染的前奏:

  1. 瀏覽器輸入url,瀏覽器主進程接管,開了一個下載線程
  2. 然後進行HTTP請求(DNS查詢、IP尋址等等),等待響應,開始下載響應報文。
  3. 將下載完的內容轉交給Renderer進程管理
  4. 開始渲染...

在說渲染之前,需要理解一些概念:

  • DOM Tree: 瀏覽器將HTML解析成樹形的數據結構。
  • CSS Rule Tree:瀏覽器將CSS解析成樹形的數據結構。
  • Render Tree:DOM樹和CSS規則樹合併後生產Render樹。
  • layout:有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及他們的從屬關係,從而去計算出每個節點在屏幕中的位置。
  • painting: 按照算出來的規則,通過顯卡,把內容畫到屏幕上。
  • reflow(迴流):當瀏覽器發現某個部分發生了點變化影響了佈局,需要倒回去重新渲染,內行稱這個回退的過程叫 reflow。reflow 會從 <html> 這個 root frame 開始遞歸往下,依次計算所有的結點幾何尺寸和位置。reflow 幾乎是無法避免的。現在界面上流行的一些效果,比如樹狀目錄的摺疊、展開(實質上是元素的顯 示與隱藏)等,都將引起瀏覽器的 reflow。鼠標滑過、點擊……只要這些行爲引起了頁面上某些元素的佔位面積、定位方式、邊距等屬性的變化,都會引起它內部、周圍甚至整個頁面的重新渲 染。通常我們都無法預估瀏覽器到底會 reflow 哪一部分的代碼,它們都彼此相互影響着。
  • repaint(重繪):改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內部佈局的屬性時,屏幕的一部分要重畫,但是元素的幾何尺寸沒有變。

注意:display:none的節點不會被加入Render Tree,而visibility: hidden則會,所以display:none會觸發reflowvisibility: hidden會觸發repaint

瀏覽器內核拿到響應報文之後,渲染大概分爲以下步驟

  1. 解析html生產DOM樹。
  2. 解析CSS規則。
  3. 根據DOM Tree和CSS Tree生成Render Tree。
  4. 根據Render樹進行layout,負責各個元素節點的尺寸、位置計算。
  5. 繪製Render樹(painting),繪製頁面像素信息。
  6. 瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上。

詳細步驟略去,大概步驟如下,渲染完畢後JS引擎開始執行load事件,繪製流程見下圖。

圖片描述

由圖中可以看出,css在加載過程中不會影響到DOM樹的生成,但是會影響到Render樹的生成,進而影響到layout,所以一般來說,style的link標籤需要儘量放在head裏面,因爲在解析DOM樹的時候是自上而下的,而css樣式又是通過異步加載的,這樣的話,解析DOM樹下的body節點和加載css樣式能儘可能的並行,加快Render樹的生成的速度,當然,如果css是通過js動態添加進來的,會引起頁面的重繪或重新佈局。
從有html標準以來到目前爲止(2017年5月),標準一直是規定style元素不應出現在body元素中。

前面提到了load事件,那麼與DOMContentLoaded事件有什麼分別。

  • 當 DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片。 (譬如如果有async加載的腳本就不一定完成)
  • 當 onLoad 事件觸發時,頁面上所有的DOM,樣式表,腳本,圖片都已經加載完成了。 (渲染完畢了)

順序是:DOMContentLoaded -> load

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章