深入前端-徹底搞懂瀏覽器運行機制

瀏覽器是多進程的

Browser進程:

瀏覽器的主進程(負責協調、主控),只有一個。

負責瀏覽器界面顯示,與用戶交互。如前進,後退等

負責各個頁面的管理,創建和銷燬其他進程

將Renderer進程得到的內存中的Bitmap,繪製到用戶界面上

網絡資源的管理,下載等

第三方插件進程:

每種類型的插件對應一個進程,僅當使用該插件時才創建

GPU進程:

最多一個,用於3D繪製等

瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的):

默認每個Tab頁面一個進程,互不影響。主要作用爲
頁面渲染,腳本執行,事件處理等

新建頁面都會開啓一個新的進程

上面的進程會輔助這個進程的執行,其中渲染進程對頁面的影響最重要

怎麼查看瀏覽器進程情況

打開Chrome Shift+Esc;我們可以看到進程情況,Chrome圖標就是Browser主進程

多線程優缺點

避免單個page crash影響整個瀏覽器

避免第三方插件crash影響整個瀏覽器

多進程充分利用多核優勢

方便使用沙盒模型隔離插件等進程,提高瀏覽器穩定性

簡單點理解:如果瀏覽器是單進程,那麼某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理如果是單進程,插件崩潰了也會影響整個瀏覽器;而且多進程還有其它的諸多優勢,當然內存等資源消耗也會更大

重點是瀏覽器內核(渲染進程)該進程有多個線程完成

GUI渲染線程

負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。
當界面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該線程就會執行
注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(相當於被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閒時立即被執行。

JS引擎線程

也稱爲JS內核,負責處理Javascript腳本程序。(例如V8引擎)
JS引擎線程負責解析Javascript腳本,運行代碼。
JS引擎一直等待着任務隊列中任務的到來,然後加以處理,一個Tab頁(renderer進程)中無論什麼時候都只有一個JS線程在運行JS程序
同樣注意,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞。

事件觸發線程

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

定時觸發器線程

傳說中的setInterval與setTimeout所在線程
瀏覽器定時計數器並不是由JavaScript引擎計數的,(因爲JavaScript引擎是單線程的, 如果處於阻塞線程狀態就會影響記計時的準確)
因此通過單獨線程來計時並觸發定時(計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行)
注意,W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms。

異步http請求線程

在XMLHttpRequest在連接後是通過瀏覽器新開一個線程請求
將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。

進程間的通信過程

  1. Browser進程收到用戶請求,首先需要獲取頁面內容(譬如通過網絡下載資源),隨後將該任務通過RendererHost接口傳遞給Render進程
  2. Renderer進程的Renderer接口收到消息,簡單解釋後,交給渲染線程,然後開始渲染
  3. 渲染線程接收請求,加載網頁並渲染網頁,這其中可能需要Browser進程獲取資源和需要GPU進程來幫助渲染
  4. 當然可能會有JS線程操作DOM(這樣可能會造成迴流並重繪)
  5. 最後Render進程將結果傳遞給Browser進程
  6. Browser進程接到結果並將結果繪製出來

渲染進程(瀏覽器內核)線程的關係

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

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

JS阻塞頁面加載

從上述的互斥關係,可以推導出,JS如果執行時間過長就會阻塞頁面。
譬如,假設JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被保存到隊列中,等待JS引擎空閒後執行。
然後,由於巨量計算,所以JS引擎很可能很久很久後才能空閒,自然會感覺到巨卡無比。
所以,要儘量避免JS執行時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞的感覺。

WebWorker,JS的多線程

JavaScript引擎是單線程運行的,JavaScript中耗時的I/O操作都被處理爲異步操作,它們包括鍵盤、鼠標I/O輸入輸出事件、窗口大小的resize事件、定時器(setTimeout、setInterval)事件、Ajax請求網絡I/O回調等。當這些異步任務發生的時候,它們將會被放入瀏覽器的事件任務隊列中去,等到JavaScript運行時執行線程空閒時候纔會按照隊列先進先出的原則被一一執行,但終究還是單線程。

創建Worker時,JS引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,完全受主線程控制,而且不能操作DOM)
JS引擎線程與worker線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數據)

//主線程 main.js
var worker = new Worker("worker.js");
worker.onmessage = function(event){
    // 主線程收到子線程的消息
};
// 主線程向子線程發送消息
worker.postMessage({
    type: "start",
    value: 12345
});

//web worker.js
onmessage = function(event){
   // 收到
};
postMessage({
    type: "debug",
    message: "Starting processing..."
});

瀏覽器渲染流程

拿到內容

  • 瀏覽器根據 DNS 服務器得到域名的 IP 地址
  • 向這個 IP 的機器發送 HTTP 請求
  • 服務器收到、處理並返回 HTTP 請求
  • 瀏覽器得到返回內容

解析內容建立Rendering Tree

解析HTMl構建dom

  1. dom作用: HTMLDOM是HTML Document Object Model(文檔對象模型)的縮寫,HTML DOM則是專門適用於HTML/XHTML的文檔對象模型。熟悉軟件開發的人員可以將HTML DOM理解爲網頁的API。它將網頁中的各個元素都看作一個個對象,從而使網頁中的元素也可以被計算機語言獲取或者編輯。
  2. dom規定
  • 整個文檔是一個文檔節點
  • 每個HTML標籤是一個元素節點
  • 包含在HTML元素中的文本是文本節點
  • 每一個HTML屬性是一個屬性節點(屬性節點是另一個層面的理解,在瀏覽器後臺打印的時候,不存在屬性節點)
  • 註釋屬於註釋節點
  1. 解析過程:瀏覽器會自動把HTML文檔解析爲一個“文檔對象模型”,即Document Object Model,簡稱DOM,這是一個樹形結構,樹根是Document對象,樹幹是網頁的根元素<html>,然後分出兩個枝丫,一個是<head>,一個是<body>,然後網頁上的其他標籤就是這棵樹上的樹葉和樹枝了,通過這個結構,就可以查找和控制網頁上的任何一個元素了。因此,可以這麼說,網頁上的任何元素都是Document對象的子對象。

解析CSS產生CSS規則樹,css是由單獨的下載線程異步下載的,本身不會阻塞Dom加載,它和DOM結構比較像然後結合DOM生成RenderTree

Javascript解析, 通過 DOM API 和 CSSOM API 來操作 DOM Tree 和 CSS Rule Tree。

佈局render樹(Layout/reflow),負責各元素尺寸、位置的計算

繪製render樹(paint),繪製頁面像素信息

瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上

渲染過程中的問題

DOMContentLoaded與onload

  • 當 DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片
  • 當 onload 事件觸發時,頁面上所有的DOM,樣式表,腳本,圖片都已經加載完成了。 (渲染完畢了)
  • DOMContentLoaded -> load

瀏覽器如果渲染過程中遇到JS文件怎麼處理?

  • 上面說過GUI渲染線程與JS引擎線程是互斥的,所以渲染過程中,如果遇到<script>就停止渲染,執行 JS 代碼,也就是說,在構建DOM時,HTML解析器若 遇到了JavaScript,那麼它會暫停構建DOM,將控制權移交給JavaScript引擎,等JavaScript引擎運行完畢,瀏覽器再從中斷的地方恢復DOM構建。
  • 首屏渲染的越快,就越不應該在首屏就加載 JS 文件,這也是都建議將 script 標籤放在 body 標籤底部的原因

JS文件不只是阻塞DOM的構建,它會導致CSSOM也阻塞DOM的構建。

  • 因爲JavaScript不只是可以改DOM,它還可以更改樣式,也就是它可以更改CSSOM。因爲不完整的CSSOM是無法使用的,如果JavaScript想訪問CSSOM並更改它,那麼在執行JavaScript時,必須要能拿到完整的CSSOM。
  • 所以就導致了一個現象,如果瀏覽器尚未完成CSSOM的下載和構建,而我們卻想在此時運行腳本,那麼瀏覽器將延遲腳本執行和DOM構建,直至其完成CSSOM的下載和構建。也就是說,在這種情況下,瀏覽器會先下載和構建CSSOM

<script src="script.js"></script>

  • 沒有 defer 或 async,瀏覽器會立即加載並執行指定的腳本,也就是說不等待後續載入的文檔元素,讀到就加載並執行。

<script defer src="script.js"></script>(延遲執行)

  • defer 屬性表示延遲執行引入的 JavaScript,即這段 JavaScript 加載時 HTML 並未停止解析,這兩個過程是並行的。整個 document 解析完畢且 defer-script 也加載完成之後(這兩件事情的順序無關),會執行所有由 defer-script 加載的 JavaScript 代碼,然後觸發 DOMContentLoaded 事件。
  • defer 與相比普通 script,有兩點區別:載入 JavaScript 文件時不阻塞 HTML 的解析,執行階段被放到 HTML 標籤解析完成之後。 在加載多個JS腳本的時候,async是無順序的加載,而defer是有順序的加載。

<script async src="script.js"></script> (異步下載)

  • async 屬性表示異步執行引入的 JavaScript,與 defer 的區別在於,如果已經加載好,就會開始執行。也就是加載不阻塞,執行會阻塞。

瀏覽器的迴流與重繪 (Reflow & Repaint)

瀏覽器使用流式佈局模型 (Flow Based Layout)。
有了RenderTree,我們就知道了所有節點的樣式,然後計算他們在頁面上的大小和位置,最後把節點繪製到頁面上。

迴流

當Render Tree中部分或全部元素的尺寸、結構、或某些屬性發生改變時,瀏覽器重新渲染部分或全部文檔的過程稱爲迴流。

  • 頁面首次渲染
  • 瀏覽器窗口大小發生改變
  • 元素尺寸或位置發生改變
  • 元素內容變化(文字數量或圖片大小等等)
  • 元素字體大小變化
  • 添加或者刪除可見的DOM元素
  • 激活CSS僞類(例如::hover)
  • 查詢某些屬性或調用某些方法

重繪

當頁面中元素樣式的改變並不影響它在文檔流中的位置時(例如:color、background-color、visibility等),瀏覽器會將新樣式賦予給元素並重新繪製它,這個過程稱爲重繪。

參考文章

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