webkit理論知識

瀏覽器進程與線程

進程

瀏覽器是多進程模型。chrome瀏覽器主要包括如下進程類型javascript

  1. Brower進程:瀏覽器的主進程,負責瀏覽器界面的顯示,各個頁面的管理,是全部其餘類型進程的祖先,主要負責他們的建立和銷燬工做,它有且僅有一個
  2. Render進程:網頁的渲染進程,負責頁面的渲染工做。Renderer進程的數量是不固定的,各個瀏覽器能夠有不一樣的配置。默認狀況下爲process-per-tab,即爲每個標籤頁建立一個獨立的進程,而無論它們是不是不一樣域不一樣實例。咱們使用的chrome瀏覽器默認一個標籤對應一個進程,可是若是是從一個頁面打開了新頁面,而新頁面和當前頁面屬於同一個站點時,那麼新頁面會複用父頁面的進程。(同一站點定義爲根域名加上協議一致即爲同一個站點)。爲何要共用一個渲染進程呢?由於他們會共享JS的執行環境,例如新頁面可使用window.opener.location.href=「」控制父頁面的連接。除非在打開新頁面的時候使用了rel="noopener noreferrer",此時會是獨立的進程,新頁面也拿不到window.opener了
  3. NPAPI插件進程:該進程是爲NPAPI類型的插件而建立的。其建立的原則是每種類型的插件只會被建立一次,並且僅當使用時纔會被建立。當有多個網頁須要使用同一種類型的插件時,進程會爲每個使用者建立一個實例,插件進程是被共享的。
  4. GPU進程:最多隻有一個,並且僅當GPU硬件加速打開時會被建立,主要是對3D圖形加速調用的實現。
  5. Pepper插件進程:同NPAPI插件進程,不一樣的是爲Pepper插件而建立的進程
  6. 其它類型的進程:例如Linux下的Zygote進程,另外就是Sandbox的準備進程、

進程模型有如下特徵:html

  1. Brower進程和頁面的渲染是分開的,這保證了頁面渲染致使的崩潰不會致使瀏覽器主界面的崩潰
  2. 每一個網頁是獨立的進程,這保證了頁面之間相互不影響
  3. 插件進程也是獨立的,插件自己的問題不會影響瀏覽器主界面和網頁
  4. GPU硬件加速進程也是獨立的。

經過chrome瀏覽器右上角的三個點--More Tools--Task Manager能夠查看當前瀏覽器所開啓的進程。注:三個tab共享一個進程的狀況是在第一個Tab打開了另外兩個tabjava

瀏覽器開啓的進程

線程

每個進程內部,都有不少線程。web

多線程模型

Browser進程下有不少線程:
Xnip2020-01-16_16-30-30.jpgchrome

其中線程1 Chrome是主線程。Chrome IOThread線程就是IO線程。中間還有用來處理視頻、存儲、王闊、文件、音頻、瀏覽歷史等的線程。canvas

Render進程下有如下線程
render進程所包含的線程瀏覽器

其中線程Chrome是主線程,Chrome IOThread線程就是IO線程。線程2是一個新的線程,用來解釋HTML文檔。緩存

網頁的加載和渲染過程的基本工做方式以下:服務器

  1. Browser進程收到用戶的請求,首先由UI線程處理,並且將相應的任務轉達給IO線程,它隨即將該任務傳遞給Renderer進程。
  2. Renderer進程的IO線程通過簡單解釋後交給渲染線程。渲染線程接受請求,加載網頁並渲染網頁,這其中可能須要Browser進程獲取資源和須要GPU進程來幫助渲染。最後Render進程將結果由IO線程傳遞給Browser進程。
  3. 最後,Browser進程接受到結果並繪製

瀏覽器的資源加載

HTML支持的資源大體有:Html/Js/CSS/圖片/svg/視頻、音頻等,資源在加載過程當中分爲在緩存中和不在緩存中兩種狀況。網絡

例如在解析Html過程當中,發現一個img標籤,webkit會專門建立一個ImageLoader去加載該資源。因爲獲取資源耗時較長,一般是異步執行的,也就是說資源的獲取和加載不會阻礙當前Webkit的渲染過程,例如圖片/CSS。

固然某些資源例如JS會阻礙主線程的渲染。Webkit會怎麼作呢?當前的主線程被阻礙時,webkit會另起一個線程去遍歷後面的HTML網頁,收集須要的資源URL,發送請求。這樣就能夠避免被阻礙。與此同時,Webkit可以併發下載這些資源,甚至併發下載JS代碼資源,這種機制對於網頁的加速加載非常明顯。

這個地方說法跟一般講的將script標籤加上async屬性或者放在body結束標籤的前面來提高性能的說法不太一致。書中給出的解釋是,就算webkit有本身的優化策略,但仍是建議加上async屬性或者放在body結束標籤的前面,由於並非全部的渲染引擎都做了如此的考慮

緩存相關

緩存資源池是有限的,必須有響應的機制來替換其中的資源,這個機制就是LRU(Last Recent Used)最少使用原則。

網絡請求中,DNS解析和TCP鏈接佔用大量的時間。網頁開發者能夠從如下方面着手減小這一部分時間

  • 減小連接重定向
  • 利用DNS預解析 <Link rel="dns-prefetch" href="...">
  • 搭建支持SPDY協議的服務器
  • 避免錯誤的連接請求,失效的連接也會佔用網絡資源

減小資源的數量

  • 內嵌小型的資源,例如JS和CSS,減小網絡請求。圖片轉爲base64的等
  • 合併資源,利用雪碧圖等
  • 利用瀏覽器緩存

Html解析器

在Render進程中有一個線程,該線程用來處理HTML文檔的解釋任務。由於JS代碼可能會修改文檔結構,因此JS代碼的執行會阻塞後面節點的建立,同時也會阻礙後面的資源下載。因此有兩點建議

  • 將script標籤加上async屬性,代表該腳本能夠異步執行
  • 將script標籤放在body元素的最後。

建議1、

<html>
<head>
    <script type="" async>
    ...
    </script>
</head>
<body>
    <img src="" />
</body>
<html>

建議2、

<html>
<head>
</head>
<body>
    <img src="" />
    <script type="">
    ...
    </script>
</body>
<html>

但其實在執行JS代碼時,webkit有本身的優化機制,webkit會先暫停JS執行,掃描後面的詞語,若是發現有其它資源,使用預資源加載器來發送請求,在這以後才執行JS代碼。儘管如此,仍是推薦按建議的寫代碼,畢竟不是全部的渲染引擎都作了考慮。

事件機制

事件分爲3個階段,1事件捕獲階段,2處於目標事件階段,3冒泡階段,可使用event.phase獲取當前所屬的階段。使用addEventListner默認是在冒泡階段捕獲事件,除非最後一個參數制定個爲true;

大多數狀況下,都是將事件處理程序添加到事件流的冒泡階段,能夠最大程度兼容各類瀏覽器。最好只在須要在事件到達目標以前捕獲它時才添加到捕獲階段。
<html>
    <body id="body">
        <div id="div">
            <span id="span">span元素</span>
        </div>
        <script type="text/javascript">
        function onSpan(event) {
            console.log('on span');
        }
        function onDiv(event) {
            console.log('on div');
        }
        function onBody(event) {
            console.log('on body');
        }
        window.onload = function () {
            const spanEle = document.getElementById('span');
            spanEle.addEventListener('click', onSpan);
            const divEle = document.getElementById('div');
            divEle.addEventListener('click', onDiv);
            const bodyEle = document.getElementById('body');
            bodyEle.addEventListener('click', onBody, true);
        }
        </script>
    </body>
</html>

點擊span後代碼執行順序爲 body、 span、 div

瀏覽器渲染

在chrome的console中使用document查看當前頁面的DOM樹結構。當渲染引擎接收到 CSS 文本時,會執行一個轉換操做,將 CSS 文本轉換爲瀏覽器能夠理解的結構——styleSheets。在控制檯中輸入 document.styleSheets就能夠看到對應的結構。總結以下

  • 瀏覽器不能直接理解 HTML 數據,因此第一步須要將其轉換爲瀏覽器可以理解的 DOM 樹結構;
  • 生成 DOM 樹後,還須要根據 CSS 樣式表,來計算出 DOM 樹全部節點的樣式;
  • 最後計算 DOM 元素的佈局信息,使其都保存在佈局樹中。

網頁層次

由於頁面中有不少複雜的效果,如一些複雜的 3D 變換、頁面滾動、video節點,或者使用 z-indexing 作 z 軸排序等,爲了更加方便地實現這些效果,渲染引擎還須要爲特定的節點生成專用的圖層,並生成一棵對應的圖層樹(LayerTree)。

具體如下狀況會生成單獨的合成層:

  • 具備CSS 3D屬性或者CSS透視效果
  • 節點是使用硬件加速的視頻解碼技術的HTML5 video元素
  • 節點是使用硬件加速的canvas 2D元素或者WebGL技術
  • 使用了硬件加速的CSS filters技術
  • 使用了剪裁(Clip)或者反射(Reflection)屬性,而且後代中包含一個合成層
  • 有一個Z座標比本身小的兄弟節點,而且該節點是一個合成層

每一個RenderLayer對象能夠被想象成圖像中的一個層,各個層一同構成了一個圖像,在渲染過程當中,每一個層對應網頁中的一個或者一些可視元素,這些元素繪製內容到該層上,把這個過程稱爲繪圖操做。若是繪圖操做須要GPU完成,稱之爲GPU硬件加速繪圖。理想狀況下,每一個層都有個繪製的存儲區域,這個存儲區域用來保存繪圖的結果。最後,須要將這些層的內容合併到同一個圖像中,稱之爲合成。

網頁分層有兩個緣由:一是爲了方便網頁開發者開發網頁並設置網頁的層次;二是爲了webkit處理上的遍歷,也就是爲了簡化渲染邏輯。

從輸入網頁URL到構建完DOM樹這個過程:

  • 當用戶輸入URL的時候,Webkit調用其資源加速器加載該URL對應的網頁
  • 加載器依賴網絡模塊創建鏈接,發送情感求並接收答覆
  • Webkit接收到各類網頁或者資源的數據,其中某些資源多是同步或異步的
  • 網頁被交給HTML解釋器轉變成一系列的詞語
  • 解釋器根據詞語構建節點,造成DOM樹
  • 若是節點是JS代碼的話,調用JS引擎解釋並執行
  • JS代碼可能會修改DOM樹的結構
  • 若是節點須要依賴其餘資源,例如圖片、CSS、視頻等,調用資源加載器來加載它們,可是它們是異步的,不會阻礙當前DOM樹的繼續構建,若是是JS資源URL(沒有標記異步方式),則須要中止當前DOM樹的構建,直到JS資源被加載並被JS引擎執行後繼續DOM樹的構建

接下來就是Webkit利用CSS和DOM樹構建RenderObject樹直到繪圖上下文,具體過程以下:

  • CSS文件被CSS解釋器解釋成內部表示結構
  • CSS解釋器工做完以後,在DOM樹上附加解釋後的樣式信息,這就是RenderObject樹
  • RenderObject節點在建立的同時,Webkit會根據網頁的層次結構建立RenderLayer樹,同時構建一個虛擬的繪圖上下文。
  • 最後就是根據繪圖上下文來生成最終的圖像,這一過程主要依賴2D和3D圖形庫。這一過程還會涉及到GPU硬件渲染、混合渲染模型等方式

再看重繪、重排和合成

網頁加載後,每當從新繪製新的一幀時,通常須要通過三個階段:計算佈局--繪圖--合成。其中前兩個階段比較耗時間,合成時間相對要少一些。在實際應用中,能夠經過以下方法來減小webkit繪製每一幀所須要的時間:1、使用合適的網頁分層技術以減小須要從新計算的佈局和繪圖;2、使用CSS 3D變形和動畫技術。

  • 重繪:更改元素的尺寸,例如元素的寬高,那麼瀏覽器會觸發計算佈局、繪圖和合成三個階段。
  • 重排:更改元素的背景色,沒有幾何尺寸改變,會省去構建RenderObject樹和RenderLayer樹階段,直接到繪製、合成階段
  • 合成:例如使用了 CSS 的 transform 來實現動畫效果,會直接到最後一步的合成階段

JS引擎

爲何說JS效率低?

JS語言的一個特色是它是無類型語言,沒有辦法在編譯的時候知道變量類型,運行的時候才能肯定。在運行時計算和決定類型,會帶來很嚴重的性能損耗。

這相較於靜態語言例如C++的區別是,靜態語言只須要知道變量的地址及類型,地址加上類型的長度,就能夠得出該變量的值。

  • 編譯肯定位置:C++有明確的兩個階段,編譯這些位置的偏移信息都是編譯器在編譯階段就決定了的。當C++代碼編譯成本地代碼以後,對象的屬性和偏移信息都計算完成。而JS沒有類型,只有在對象建立的時候纔有這些信息,於是只能在執行階段肯定。
  • 偏移信息共享:C++由於有類型定義,因此全部對象都是該類型來肯定的,並且執行的時候不能動態改變類型。因此訪問它們只須要按照編譯時肯定的偏移量便可。JS則不一樣,每一個對象都是自描述,屬性和偏移位置信息都包含在自身結構中。
  • 偏移信息查找:C++中查找偏移地址很簡單,都是在編譯代碼時,對使用到的某類型成員變量直接設置偏移量。而JS代碼使用到一個對象則須要經過屬性名匹配才能找到對應的值

JS的編譯和執行

JS引擎就是可以將JS代碼處理並執行的運行環境。JS引擎執行過程主要分爲三個階段,分別是語法分析,預編譯和執行階段。

語法分析

語法分析就是經過詞法分析和語法分析獲得語法樹的過程,若是在構造語法樹的時候,發現錯誤,就會報錯並結束整個代碼塊的解析。

預編譯階段

首先了解變量對象(Variable Object, 縮寫爲VO)是用於存儲執行上下文中的: 


  • 變量
  • 函數聲明
  • 函數參數

在函數上下文中,變量對象被表示爲活動對象AO;

function test(a, b) {  
    var c = 10;  
    function d() {}  
    var e = function _e() {}; 
    (function x() {});
    b=20;
}

test(10)

VO按照以下順序填充:

  • 函數參數(若未傳⼊入,初始化該參數值爲undefined)
  • 函數聲明(若發⽣生命名衝突,會覆蓋)
  • 變量聲明(初始化變量值爲undefined,若發⽣生命名衝突,會忽略。

所以

AO(test) = {  
    a: 10,  
    b: undefined,  
    c: undefined,  
    d: <ref to func "d"\>
    e: undefined
};

代碼執行階段

AO(test) = {  
    a: 10,  
    b: 20,  
    c: 10,
    d: <reference to FunctionDeclaration "d"\>
    e: function \_e() {};
};

參考文檔

  • 《webkit技術內幕》
  • 極客時間 《瀏覽器工做原理與實踐》
相關文章
相關標籤/搜索