瀏覽器運行機制詳解


前言

 大家肯定都聽說過很多瀏覽器優化原則吧,例如說減少DOM操作,使用transformX(0)進行硬件優化,避免js文件執行時間過長使得頁面卡頓等等。大部分人可能都知道,但也僅限於知道,即知其然,不知其所以然。

 學習要形成自己的知識體系,否則的話,往往是東一榔頭西一榔頭地學習知識,這樣導致學習到的知識松散,無法形成內在的聯系,也就導致了學習地不夠深入,只是浮於表面,只是“記住”了知識。

 所以,接下來,我想來為大家梳理一下瀏覽器運行過程中需要理解的知識,如下:

  • 前言
  • 進程與線程
  • 瀏覽器進程
    • 瀏覽器都有哪些進程
    • 瀏覽器內核(renderer進程)
    • html解析
    • css解析
    • render樹
    • 回流與重繪
      • 什么時候會發生回流與重繪
      • 具體什么操作會引起回流
      • 如何減少回流
    • 硬件加速
      • 如何才能使用硬件加速
      • 硬件加速使用z-index
    • 瀏覽器頁面的渲染流程
    • DOMContentLoaded和load事件
    • css堵塞情況
    • js堵塞情況
    • css和js文件應當放在html哪個位置
    • 事件循環機制
    • 宏任務和微任務
    • 導致頁面無法響應的原因
    • html文件解析過程
  • 參考鏈接

# 進程與線程

 可以這樣理解:


 - 進程是一個工廠,每個工廠有其獨立的資源。

 - 線程是工廠中的工人,可能只有一個,可能有好多個。多個工人協同完成工作。工人共享工作資源。



 回到硬件上來理解:

 - 工廠的資源 -> 系統分配的內存。

 - 工廠之間相互獨立 -> 進程之間相互獨立,也即進程分配到的內存相互獨立,無法讀到對方內存中的數據。

 - 一個工廠有一個或多個工人 -> 一個線程中有一個或多個線程。

 - 多個工人協同完成工作 -> 進程中多個線程協同完成工作。即線程之間能互相發送請求與接收結果。

 - 工人共享工作資源 -> 進程中所有線程都能訪問到相同一塊內存,即信息是互通的。



 不過在這里要強調一點:**一個軟件不等於一個進程,一個軟件可能包含有多個互相獨立的進程。**

 最后,再用官方的術語描述下進程與線程的差別


- 進程是系統資源分配的最小單位(即系統以進程為最小單位分配內存空間,同時進程是能獨立運行的最小單位)

- 線程是系統調度的最小單位(即系統以線程為單位分配cpu中的核。)
 tips:
- 進程之間也能互相通信,不過代價比較大。

瀏覽器進程

 首先,明確的是:瀏覽器是多線程的。

 以Chrome瀏覽器為例:

 大家有興趣的話,也可以打開Chrome的任務管理器測試。由圖可知,Chrome中有多個進程(每個tab頁面對應一個進程,以及Browser進程,GPU進程和插件進程)。


## 瀏覽器都有哪些進程

瀏覽器中的進程分別是:


- Browser進程 : 是瀏覽器的主進程,負責主控,協調,只有一個,可以看做是瀏覽器的大腦。 - 負責下載頁面的網絡文件 - 負責將 renderer進程得到的存在內存中的位圖渲染(顯示)到頁面上 - 負責創建和銷毀tab進程(renderer進程) - 負責與用戶的交互 - GPU進程 : 只有一個。 - 負責3D繪制,只有當該頁面使用了硬件加速才會使用它,來渲染(顯示)頁面。否則的話,不使用這個進程,而是用Browser進程來渲染(顯示)頁面 - renderer進程:又名瀏覽器內核,每個tab頁面對應一個獨立的renderer進程,內部有多個線程。 - 負責腳本執行,位圖繪制,事件觸發,任務隊列輪詢等 - 第三方插件進程:每種類型的插件對應一個進程。
 瀏覽器是多進程的好處非常明顯,**如果瀏覽器是單線程的話,則一個頁面,一個插件的崩潰會導致整個瀏覽器崩潰,用戶體驗感會非常差。**
## 瀏覽器內核(renderer進程)

 ,弄懂了這一部分的知識,那么你對一個網頁的運行機制也就能有個框架了。

 renderer進程是多線程的,以下是各個線程的名稱及作用(僅列舉常駐線程):


- js引擎線程: - 也稱js內核,解析js腳本,執行代碼 - 與GUI線程互斥,即當js引擎線程運行時,GUI線程會被掛起,當js引擎線程結束運行時,才會繼續運行GUI線程 - 由一個主線程和多個web worker線程組成,由於web worker是附屬於主線程,無法操作dom等,所以js還是單線程語言(在主線程運行js代碼) - GUI渲染線程: - 用於解析html為DOM樹,解析css為CSSOM樹,布局layout,繪制paint - 當頁面需要重排reflow,重繪repaint時,使用該線程 - 與js引擎線程互斥 - 事件觸發線程 - 當對應事件觸發(不論是WebAPIs完成事件觸發,還是頁面交互事件觸發)時,該線程會將事件對應的回調函數放入callback queue(任務隊列)中,等待js引擎線程的處理 - 定時觸發線程 - 對應於setTimeout,setInterval API,由該線程來計時,當計時結束,將事件對應的回調函數放入任務隊列中 - 當setTimeout的定時的時間小於4ms,一律按4ms來算 - http請求線程 - 每有一個http請求就開一個該線程 - 當檢測到狀態變更的話,就會產生一個狀態變更事件,如果該狀態變更事件對應有回調函數的話,則放入任務隊列中 - 任務隊列輪詢線程 - 用於輪詢監聽任務隊列,以知道任務隊列是否為空
 想必大家對renderer進程里的組成及職能有個大概的認知了,接下來,我們會着重於細節來進行研究。
## html解析

 html解析包含有一系列的步驟,過程為**Bytes → Characters → Tokens → Nodes → DOM。**最終將html解析為DOM樹。

 假設有一html頁面,代碼如下:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

 處理過程如下:

 最終生成的DOM樹:


## css解析

 與html解析類似,他解析最終形成CSSOM樹,過程為Bytes → Characters → Tokens → Nodes → CSSOM。

 假設css代碼如下:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

 得到的CSSOM為:

render樹

 由DOM樹與CSS樹結合形成的渲染樹(其中無法顯示的元素,如script,head元素或diplay:none的元素,不會在渲染樹中,也就最終不會被渲染出來),頁面的布局,繪制都是以render樹為依據。

 由以上的DOM樹與CSSOM樹,最終得到的渲染樹如下:


## 回流與重繪

 在此之前,我們先明確另外兩個概念:布局與繪制。



 - 布局是頁面首次加載時進行的操作,重新布局即為回流。

 - 繪制是頁面首次加載時進行的操作,重新繪制即為重繪。

什么時候會發生回流和重繪呢:


- 當頁面的某部分元素發生了尺寸、位置、隱藏發生了改變,頁面進行回流。 得對整個頁面重新進行布局計算,將所有尺寸,位置受到影響的元素回流。 - 當頁面的某部分元素的外觀發生了改變,但尺寸、位置、隱藏沒有改變,頁面進行重繪。(同樣,只重繪部分元素,而不是整個頁面重繪)
 **回流的同時往往會伴隨着重繪,重繪不一定導致回流。**所以回流導致的代價是大於重繪的。

 如果大家對這兩者的差別還不是很清楚的話,我引用這兩張圖給大家:

回流

重繪

那么具體什么操作會引起回流呢:


- 頁面初始化渲染 - 窗口的尺寸變化 - 元素的尺寸、位置、隱藏變化 - DOM結構發生變化,如刪除節點 - 獲取某些屬性,引發回流 - 很多瀏覽器會對回流進行優化,一定時間段后或數量達到闋值時,做一次批處理回流。 - 當獲取一些屬性時,瀏覽器為了返回正確的值也會觸發回流,導致瀏覽器優化無效,有: 1. offset(top/bottom/left/right) 2. client (top/bottom/left/right) 3. scroll (top/bottom/left/right) 4. getComputedStyle() 5. width,height - 其次,字體大小修改及內容更新也會導致回流
 頻繁的回流與重繪會導致頻繁的頁面渲染,導致cpu或gpu過量使用,使得頁面卡頓。

那么如何減少回流呢:


1. 減少逐項更改樣式,最好一次性更改style,或是將更改的樣式定義在class中並一次性更新 2. 避免循環操作DOM,而是新建一個節點,在他上面應用所有DOM操作,然后再將他接入到DOM中 3. 當要頻繁得到如offset屬性時,只讀取一次然后賦值給變量,而不是每次都獲取一次 4. 將復雜的元素絕對定位或固定定位,使他脫離文檔流,否則回流代價很高 5. 使用硬件加速創建一個新的復合圖層,當其需要回流時不會影響原始復合圖層回流
## 硬件加速

 我們在未開啟硬件加速的時候是使用cpu來渲染頁面,只有開啟了硬件加速了,才會使用到GPU渲染頁面。

 在詳細講解硬件加速前,我們先來講解一下簡單圖層和復合圖層


- DOM中的每個結點對應一個簡單圖層 - 復合圖層是各個簡單圖層的合並,一個頁面一般來說只有一個復合圖層,無論你創建了多少個元素,都是在這個復合圖層中 - 其次,absolute、fixed布局,可以使該元素脫離文檔流,但還是在這個復合圖層中,所以他還是會影響復合圖層的繪制,但不會影響重排
 **當一個元素使用硬件加速后,會生成一個新的復合圖層**,這樣不管其如何變化,都不會影響原復合圖層。不過不要大量使用硬件加速,會導致資源消耗過度,導致頁面也卡。

 所以,使用了硬件加速后,會有多個復合圖層,然后多個復合圖層互相獨立,單獨布局、繪制。

如何才能使用硬件加速;


1. translate3d,translateZ
2. opacity屬性
### 硬件加速時請使用z-index

 具體原理是這樣的:

 當一個元素使用了硬件加速,在其后的元素,若z-index比他大或者相同,且absolute或fixed的屬性相同,則默認為這些元素也創建各自的復合圖層。

 所以我們人為地為這個元素添加z-index值,從而避免這種情況


## 瀏覽器頁面的渲染流程

 經過以上的學習,我們可以清楚瀏覽器的渲染過程了:


 1. 解析html得到DOM樹

 2. 解析css得到CSS樹

 3. 合並得到render樹

 4. 布局,當頁面有元素的尺寸、大小、隱藏有變化或增加、刪除元素時,重新布局計算,並修改頁面中所有受影響的部分

 5. 繪制,當頁面有元素的外觀發生變化時,重新繪制

 6. GUI線程將得到的各層的位圖(每個元素對應一個普通圖層)發送給Browser進程,由Browser進程將各層合並,渲染在頁面上


DOMContentLoaded和load事件

 這兩者的差別,由其定義就可知:


 - DOMContentLoaded:當DOM加載完成觸發

 - load:當DOM,樣式表,腳本都加載完時觸發


 所以可以知道,**DOMContentLoaded在load之前觸發**
### css的堵塞情況

 首先,是在Browser進程中下載css文件,當下載完成后,發送給GUI線程。

 其次,是在GUI線程中解析html及css,不過這兩者是並行的。

 由於css的下載和解析不會影響DOM樹,所以不會堵塞html文件的解析,但會堵塞頁面渲染。

 這樣的設計是非常合理的,如果css文件的下載和解析不會堵塞頁面渲染,那么在頁面渲染的途中或結束后發現元素樣式有變化,則又需要回流和重繪。


### js的堵塞情況

 明確的是,js文件的下載和解析執行都會堵塞html文件的解析及頁面渲染。

 因為js腳本可能會改變DOM結構,若是其不堵塞html文件的解析及頁面渲染的話,那么當js腳本改變DOM結構或元素樣式時,會引發回流和重繪,會造成不必要的性能浪費,不如等待js執行完,在進行html解析和頁面渲染。

 如果你不想js堵塞的話,則使用async屬性,這樣就可以異步加載js文件,加載完成后立即執行。


### css和js文件應當放在html哪個位置

js:



 當需要在DOM樹完成之前用js進行初始化操作的話,在head中使用js。

 如果是需要在DOM樹形成之后,即要操作DOM,則在body元素的末尾。不過也可以使用load事件。

 如果js的內容比較小,則推薦使用內部js而不是引用js,這樣可以減少http請求。


 **css:**

 一般放在head中,因為css的解析不影響html的解析,所以越早引入,越早同時解析。


## 事件循環機制

 事件循環機制在我的這篇文章有詳細的說明:https://www.cnblogs.com/caiyy/p/10362247.html

 總結一句話:

事件循環機制的核心是事件觸發線程,由於執行棧產生異步任務,異步任務完成后事件觸發線程將其回調函數傳入到任務隊列中,當執行棧為空,任務隊列將隊列頭的回調函數入執行棧,從而新的一輪循環開始。這就是稱為循環的原因。


### 宏任務和微任務

宏任務(macrotask):


 - 主代碼塊和任務隊列中的回調函數就是宏任務。

 - 為了使js內部宏任務和DOM任務能夠有序的執行,每次執行完宏任務后,會在下一個宏任務執行之前,對頁面重新進行渲染。(宏任務 → 渲染 → 宏任務)


#### 微任務(microtask):

 - 在宏任務執行過程中,執行到微任務時,將微任務放入微任務隊列中。

 - 在宏任務執行完后,在重新渲染之前執行。

 - 當一個宏任務執行完后,他會將產生的所有微任務執行完。


分別在什么場景下會產生宏任務或微任務呢:

  • 宏任務:主代碼塊,setTimeout,setInterval(任務隊列中的所有回調函數都是宏任務)
  • 微任務:Promise

## 導致頁面無法立即響應的原因

 導致頁面無法響應的原因是執行棧中還有任務未執行完,或者是js引擎線程被GUI線程堵塞。


## html文件解析過程

 這個過程是在下載html文件之后,不包括網絡請求過程


 1. Browser進程下載html文件並將文件發送給renderer進程

 2. renderer進程的GUI進程開始解析html文件來構建出DOM

 3. 當遇到外源css時,Browser進程下載該css文件並發送回來,GUI線程再解析該文件,在這同時,html的解析也同時進行,但不會渲染(還未形成渲染樹)

 4. 當遇到內部css時,html的解析和css的解析同時進行

 5. 繼續解析html文件,當遇到外源js時,Browser進程下載該js文件並發送回來,此時,js引擎線程解析並執行js,因為GUI線程和js引擎線程互斥,所以GUI線程被掛起,停止繼續解析html。直到js引擎線程空閑,GUI線程繼續解析html。

 6. 遇到內部js也是同理

 7. 解析完html文件,形成了完整的DOM樹,也解析完了css,形成了完整的CSSOM樹,兩者結合形成了render樹

 8. 根據render樹來進行布局,若在布局的過程中發生了元素尺寸、位置、隱藏的變化或增加、刪除元素時,則進行回流,修改

 9. 根據render樹進行繪制,若在布局的過程中元素的外觀發生變換,則進行重繪

 10. 將布局、繪制得到的各個簡單圖層的位圖發送給Browser進程,由它來合並簡單圖層為復合圖層,從而顯示到頁面上

 11. 以上步驟就是html文件解析全過程,完成之后,如若當頁面有元素的尺寸、大小、隱藏有變化時,重新布局計算回流,並修改頁面中所有受影響的部分,如若當頁面有元素的外觀發生變化時,重繪


 (完)

參考鏈接

1.CSS3硬件加速也有坑http://web.jobbole.com/83575/

2.瀏覽器渲染過程、回流、重繪簡介https://blog.csdn.net/cxl444905143/article/details/42005333

3.頁面優化,談談重繪(repaint)和回流(reflow)https://www.cnblogs.com/echolun/p/10105223.html

4.你真的了解回流和重繪嗎https://www.cnblogs.com/chenjg/p/10099886.html

5.css加載會造成阻塞嗎?https://www.cnblogs.com/chenjg/p/7126822.html

6.從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理https://segmentfault.com/a/1190000012925872#articleHeader20

7.從輸入URL到頁面加載的過程?如何由一道題完善自己的前端知識體系!https://segmentfault.com/a/1190000013662126


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM