瀏覽器原理0. 前言1. 解析過程2. 渲染樹2.1 CSS樣式計算2.2 構建渲染樹3. 佈局(重要)4. 重繪與重排(重要)5. paint(繪製)6. composite(重要)7. 瀏覽器加載

本文來自於我的github

0. 前言

身爲前端,打交道最多的就是瀏覽器和node了,也是我們必須熟悉的。接下來我們講一下瀏覽器工作原理和工作過程。從url到頁面的過程,......,我們直接來到收到服務器返回內容部分開始。

先上很多人都見過的一幅圖:

還有一幅圖:

瀏覽器主要組成部分:

  • 瀏覽器引擎:在用戶界面和呈現引擎之間傳送指令。
  • 渲染引擎:負責顯示請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在屏幕上。
  • 網絡:用於網絡調用,比如 HTTP 請求。其接口與平臺無關,併爲所有平臺提供底層實現。
  • JavaScript 解釋器:用於解析和執行 JavaScript 代碼。
  • 數據存儲:瀏覽器需要在硬盤上保存各種數據,例如 Cookie、storage、indexdb。

過程:(重要)

  1. 解析過程
  2. CSS樣式計算
  3. 構建Render Tree
  4. layout:佈局。定位座標和大小,是否換行,position, overflow之類的屬性。確定了每個DOM元素的樣式規則後,計算每個DOM元素最終在屏幕上顯示的大小和位置。Web頁面中元素的佈局是相對的,因此一個元素的佈局發生變化,會聯動地引發其他元素的佈局發生變化。比如body元素的width變化會影響其後代元素的寬度。因此,佈局過程是經常發生的。
  5. paint:繪製文字、顏色、圖像、邊框和陰影等,也就是一個DOM元素所有的可視效果。一般來說,這個繪製過程是在多個層上完成的。
  6. composite:渲染層合併。頁面中DOM元素的繪製是在多個層上進行的,在每個層上完成繪製過程之後,瀏覽器會將所有層按照合理的順序合併成一個圖層,然後在屏幕上呈現。

1. 解析過程

  • 獲取請求文檔的內容後,呈現引擎將開始解析 HTML 文檔,並將各標記逐個轉化成“內容樹”上的 DOM 節點。
  • 解析外部 CSS以及style元素中的樣式數據形成呈現樹。呈現樹包含多個帶有視覺屬性(如顏色和尺寸)的矩形。這些矩形的排列順序就是它們將在屏幕上顯示的順序。呈現樹構建完畢之後,進入“佈局”處理階段,也就是爲每個節點分配一個應出現在屏幕上的確切座標。
  • 解析script標籤時,解析完畢馬上執行,並且阻塞頁面。
  • 繪製 - 呈現引擎會遍歷呈現樹,由用戶界面後端層將每個節點繪製出來。

1.1 詞法、語法分析與編譯

詞法分析器將輸入內容分解成一個個有效標記,解析器負責根據語言的語法規則分析文檔的結構來構建解析樹。詞法分析器知道如何將無關的字符(空格、換行符等)分離出來,所以我們平時寫一些空格也不會影響大局。

在語法分析的過程中,解析器會向詞法分析器請求一個標記(就是前面分解出來的標記),並嘗試將其與某條語法規則(比如標籤要閉合、正確嵌套)進行匹配。如果發現了匹配規則,解析器會將一個對應於該標記的節點添加到解析樹中,然後繼續請求下一個標記。

如果沒有規則可以匹配,解析器就會將標記存儲到內部,並繼續請求標記,直至找到可與所有內部存儲的標記匹配的規則(如div多層嵌套的情況,這樣子能找到div閉合部分)。如果找不到任何匹配規則,解析器就會引發一個異常。這意味着文檔無效,包含語法錯誤。

解析器類型有兩種:

  • 自上而下解析器:從語法的高層結構出發,嘗試從中找到匹配的結構。
  • 自下而上解析器:從低層規則出發,將輸入內容逐步轉化爲語法規則,直至滿足高層規則。將掃描輸入內容,找到匹配的規則後,將匹配的輸入內容替換成規則。如此繼續替換,直到輸入內容的結尾。部分匹配的表達式保存在解析器的堆棧中。

編譯:將源代碼編譯成機器代碼,源代碼先走完解析的過程形成成解析樹,解析樹被翻譯成機器代碼文檔,完成編譯的過程

1.2 DTD

特殊的是,恰好html不能用上面兩種解析方法。有一種可以定義 HTML 的正規格式:DTD,但它不是與上下文無關的語法,html明顯是和上下文關係緊密的。我們知道 HTML 是有點“隨意”的,對於不閉合的或者不正確嵌套標籤有可能不報錯,並且嘗試解釋成正確的樣子,具有一定的容錯機性,因此可以達到簡化網絡開發的效果。另一方面,這使得它很難編寫正式的語法。概括地說,HTML 無法很容易地通過常規解析器解析(因爲它的語法不是與上下文無關的語法),所以採用了 DTD 格式。

1.3 解析爲dom過程

解析器解析html文檔的解析樹是由 DOM 元素和屬性節點構成的樹結構。它是 HTML 文檔的對象表示,同時也是外部內容(例如 JavaScript)與 HTML 元素之間的api,其根節點是document。上面已經說到,不能使用常規的解析技術解釋html,瀏覽器就創建了自定義的解析器來解析 。對於HTML/SVG/XHTML這三種文檔,Webkit有三個C++的類對應這三種文檔,併產生一個DOM Tree。解釋html成dom的過程,由兩個階段組成:標記化和樹構建。

1.3.1 標記化算法

對於一段html:

<html>
<body>
hi
</body>
</html>
複製代碼

該算法使用狀態機來表示。每一個狀態接收來自輸入信息流的一個或多個字符,並根據這些字符更新下一個狀態。當前的標記化狀態和樹結構狀態會影響進入下一狀態的決定。

初始狀態是數據狀態。遇到字符 < 時,狀態更改爲“標記打開狀態”。接收一個字母會創建“起始標記”,狀態更改爲“標記名稱狀態”。這個狀態會一直保持到接收 > 字符,接收到將會進入“標記打開狀態”。在此期間接收的每個字符都會附加到新的標記名稱上。

  1. 比如我們先寫html標籤,先遇到<,進入“標記打開狀態”,遇到html四個字母進入“標記名稱狀態”,接着接收到了>字符,會發送當前的標記,狀態改回“數據狀態”
  2. <body> 標記也會進行同樣的處理。現在 html 和 body 標記均已發出,而且目前是“數據狀態”。接收到 hi中的 h 字符時,將創建併發送字符標記,直到接收 </body> 中的 <。我們將爲hi的每個字符都發送一個字符標記。
  3. 回到“標記打開狀態”。接收下一個輸入字符 / 時,會創建閉合標籤token,並改爲“標記名稱狀態”。我們會再次保持這個狀態,直到接收 >。然後將發送新的標記,並回到“數據狀態”。最後,</html> 輸入也會進行同樣的處理。

1.3.2 樹構建過程

在創建解析器的同時也會創建 document 對象。在樹構建階段,以 Document 爲根節點的 DOM 樹也會不斷進行修改,向其中添加各種元素。標記生成器發送的每個節點都會由樹構建器進行處理。

  1. 樹構建階段的輸入是一個來自標記化階段的標記序列。第一個模式是“initial mode”。接收 HTML 標記後轉爲“before html”模式,並在這個模式下重新處理此標記。這樣會創建一個 HTMLHtmlElement 元素,並將其附加到 Document 根對象上。
  2. 狀態改爲“before head”。此時我們接收“body”標記。由於容錯性,就算我們的沒head標籤,系統也會隱式創建一個 HTMLHeadElement,並將其添加到樹中。
  3. 進入了“in head”模式,然後轉入“after head”模式。系統對 body 標記進行重新處理,創建並插入 HTMLBodyElement,同時模式轉變爲“in body”。
  4. 接收由“hi”字符串生成的一系列字符標記。接收第一個字符時會創建並插入文本節點,而其他字符也將附加到該節點。當然還有其他節點,比如屬性節點、換行節點。我們實際場景還有外部資源以及其他各種各樣的複雜標籤嵌套和內容結構,不過原理都類似。對於中間這個過程,遇到外部資源如何處理,順序是怎樣的,後面再講。
  5. 接收 body 結束標記會觸發“after body”模式。現在我們將接收 HTML 結束標記,然後進入“after after body”模式。接收到文件結束標記後,解析過程就此結束,dom樹已經建立完畢(不是加載完畢,在DOMContentLoaded之前,document.readyState = ‘interactive ’)。

結束後,此時文檔被標註爲交互狀態,瀏覽器開始解析那些script標籤上帶有“defer”腳本,也就是那些應在文檔解析完成後才執行的腳本,文檔狀態將設置爲“完成”,執行完畢觸發DOMContentLoaded事件(當初始的 HTML 文檔被完全加載和解析完成之後,DOMContentLoaded 事件被觸發,不會等待樣式表、圖像和iframe的完成加載)。

1.4 css和js解析過程

1.4.1 css解析

解析CSS會產生CSS規則樹,前面已經說到,html不是與上下文無關的語法,而css和js是與上下文無關的語法,所以常規的解析方法都可以用。對於建立CSS 規則樹,是需要比照着DOM樹來的。CSS匹配DOM樹主要是從右到左解析CSS選擇器。解析CSS的順序是瀏覽器的樣式 -> 用戶自定義的樣式 -> 頁面的link標籤等引進來的樣式 -> 寫在style標籤裏面的內聯樣式

樣式表不會更改 DOM 樹,因此沒有必要等待樣式表並停止文檔解析。而腳本在文檔解析階段會請求樣式信息時還沒有加載和解析樣式,腳本就會獲得錯誤的回覆。Firefox 在樣式表加載和解析的過程中,會禁止所有腳本。而對於 WebKit 而言,僅當腳本嘗試訪問的樣式屬性可能受尚未加載的樣式表影響時,它纔會禁止該腳本。

1.4.2 js解析(重要)

  • 網絡整個解析的過程是同步的,會暫停 DOM 的解析。解析器遇到 script標記時立即解析並執行腳本。文檔的解析將停止,直到腳本執行完畢。
  • 如果腳本是外部的,那麼解析過程會停止,直到從網絡同步抓取資源完成後再繼續。
  • 目前瀏覽器的script標籤是並行下載的,他們互相之間不會阻塞,但是會阻塞其他資源(圖片)的下載

所以爲了用戶體驗,後來有了async和defer,將腳本標記爲異步,不會阻塞其他線程解析和執行。標註爲“defer”的script不會停止文檔解析,而是等到解析結束才執行;標註爲“async”只能引用外部腳本,下載完馬上執行,而且不能保證加載順序。

腳本的預解析:在執行腳本時,其他線程會解析文檔的其餘部分,找出並加載需要通過網絡加載的其他資源。通過這種方式,資源可以在並行連接上加載,從而提高總體速度。請注意,預解析器不會修改 DOM 樹,而是將這項工作交由主解析器處理;預解析器只會解析外部資源(例如外部腳本、樣式表和圖片)的引用。

腳本主要是通過DOM API和CSSOM API來操作DOM Tree和CSS Rule Tree.

另外,我們又可以想到一個問題,爲什麼jsonp能response一個類eval字符串就馬上執行呢?其實也是因爲普通的script標籤解析完成就馬上執行,我們在服務器那邊大概是這樣子返回: res.end('callback('+data+')')

整個過程,就是:動態創建script標籤,src爲服務器的一個get請求接口,遇到src當然馬上請求服務器,然後服務器返回處理data的callback函數這樣子的代碼。其實,我們可以看作是前端發get請求,服務端響應文檔是js文件,而且這個文件只有一行代碼:callback(data)。當然你可以寫很多代碼,不過一般沒見過有人這麼幹。

2. 渲染樹

html、css、js解析完成後,瀏覽器引擎會通過DOM Tree 和 CSS Rule Tree 來構造 Rendering Tree(渲染樹)。

  1. 在渲染樹中,會把DOM樹中沒有的元素給去除,比如head標籤以及裏面的內容,以及display:none的元素也會被去除,但是 visibility 屬性值爲“hidden”的元素仍會顯示
  2. CSS 的 Rule Tree主要是爲了完成匹配並把CSS Rule附加上渲染樹上的每個Element,也就是所謂的Frame(Firefox 將渲染樹中的元素稱爲frame,WebKit 的是呈現器或呈現對象,其實就是DOM節點,別以爲是什麼高大上的東西。 呈現器知道如何佈局並將自身及其子元素繪製出來 )。然後,計算每個Frame的位置,這通常是layout和reflow過程中發生。
  3. 一旦渲染樹構建完成,瀏覽器會把樹裏面的內容繪製在屏幕上。

需要注意的點:

  • 有一些 DOM 元素對應多個可視化對象。它們往往是具有複雜結構的元素,無法用單一的矩形來描述。如“select”元素有 3 個呈現器:一個用於顯示區域,一個用於下拉列表框,還有一個用於按鈕。如果由於寬度不夠,文本無法在一行中顯示而分爲多行,那麼新的行也會作爲新的呈現器而添加。
  • inline 元素只能包含 block 元素或 inline 元素中的一種。如果出現了混合內容,則應創建匿名的 block 呈現器,以包裹 inline 元素。所以我們平時的inline-block可以設置寬高。
  • 有一些呈現對象對應於 DOM 節點,但在樹中所在的位置與 DOM 節點不同。脫離文檔流的浮動定位和絕對定位的元素就是這樣,被放置在樹中的其他地方,並映射到真正的frame,而放在原位的是佔位frame。

2.1 CSS樣式計算

構建渲染樹之前,需要計算每一個呈現對象的可視化屬性。這是通過計算每個元素的樣式屬性來完成的。

Firefox:CSS 解析生成 CSS Rule Tree,通過比對DOM生成Style Context Tree,然後Firefox通過把Style Context Tree和其Render Tree(Frame Tree)關聯上完成樣式計算

Webkit:把Style對象直接存在了相應的DOM結點上了

樣式被js改變過的話,會重新計算樣式(Recalculate Style)。Recalculate被觸發的時,處理腳本給元素設置的樣式。Recalculate Style會計算Render樹(渲染樹),然後從根節點開始進行頁面渲染,將CSS附加到DOM上的過程。所以任何企圖改變元素樣式的操作都會觸發Recalculate,在JavaScript執行完成後才觸發的,下面將會講到的layout也是。

2.2 構建渲染樹

Firefox:系統會針對 DOM 更新註冊展示層,作爲偵聽器。展示層將框架創建工作委託FrameConstructor,由該構造器解析樣式並創建frame。

WebKit:解析樣式和創建呈現器的過程稱爲“附加”。每個 DOM 節點都有一個“attach”方法。附加是同步進行的,將節點插入 DOM 樹需要調用新的節點“attach”方法。

處理 html 和 body 標記就會構建渲染樹根節點。這個根節點呈現對象對應於 CSS 規範中所說的容器 block,這是最上層的 block,包含了其他所有 block。它的尺寸就是視口,即瀏覽器窗口顯示區域的尺寸。Firefox 稱之爲 ViewPortFrame,而 WebKit 稱之爲 RenderView。這就是文檔所指向的呈現對象。渲染樹的其餘部分以 DOM 樹節點插入的形式來構建。

3. 佈局(重要)

呈現器在創建完成並添加到渲染樹時,並不包含位置和大小信息。**計算這些值的過程**稱爲佈局(layout)或重排(repaint)。這個得記住了,記準確了!爲什麼呢?計算offsetWidth和offsetHeight的、js操作dom、改變style屬性時候,都會引發重排!

前面通過樣式計算確定了每個DOM元素的樣式,這一步就是具體計算每個DOM元素最終在屏幕上顯示的大小和位置。Web頁面中元素的佈局是相對的,因此一個元素的佈局發生變化,會聯動地引發其他元素的佈局發生變化。比如,元素的width變化會影響其後代元素的寬度。因此,layout過程是經常發生的。

HTML 是流式佈局,這意味着大多數情況下只要一次遍歷就能計算出幾何信息。處於流中靠後位置元素通常不會影響靠前位置元素的幾何特徵,因此佈局可以按從左至右、從上至下的順序遍歷文檔。座標系是相對於根節點而建立的,使用的是上座標和左座標。根呈現器的位置左邊是 0,0,其尺寸爲視口。layout過程計算一個元素絕對的位置和尺寸。Layout計算的是佈局位置信息。任何有可能改變元素位置或大小的樣式都會觸發這個Layout事件。

layout是一個遞歸的過程。它從根呈現器(對應於 HTML 文檔的 元素)開始,然後遞歸遍歷部分或所有的框架層次結構,爲每一個需要計算的呈現器計算幾何信息。所有的呈現器都有一個“layout”或者“reflow”方法,每一個呈現器都會調用其需要進行佈局的子代的 layout 方法。任何有可能改變元素位置或大小的樣式都會觸發這個Layout事件。

由於元素相覆蓋,相互影響,稍有不慎的操作就有可能導致一次自上而下的佈局計算。所以我們在進行元素操作的時候要一再小心儘量避免修改這些重新佈局的屬性。當你修改了元素的樣式(比如width、height或者position等)也就是修改了layout,那麼瀏覽器會檢查哪些元素需要重新佈局,然後對頁面激發一個reflow過程完成重新佈局。被reflow的元素,接下來也會激發繪製過程也就是重繪(repaint),最後激發渲染層合併過程,生成最後的畫面。由於元素相覆蓋,相互影響,稍有不慎的操作就有可能導致一次自上而下的佈局計算。所以我們在進行元素操作的時候要一再小心儘量避免修改這些重新佈局的屬性。

如果呈現器在佈局過程中需要換行,會立即停止佈局,並告知其父代需要換行。父代會創建額外的呈現器,並對其調用佈局。

幾種佈局模式

  1. 父呈現器確定自己的寬度。
  2. 父呈現器依次處理子呈現器,並且放置子呈現器(設置 x,y 座標)。如果有必要,調用子呈現器的佈局,這會計算子呈現器的高度。
  3. 父呈現器根據子呈現器的累加高度以及邊距和補白的高度來設置自身高度,此值也可供父呈現器的父呈現器使用。

3.1 Dirty 位系統(Dirty bit system)

爲避免對所有細小更改都進行整體佈局,瀏覽器採用了一種“dirty 位”系統。如果某個呈現器發生了更改,或者將自身及其子代標註爲“dirty”,則需要進行佈局。類似於髒檢測。

有“dirty”和“children are dirty”兩種標記方法。“children are dirty”表示儘管呈現器自身沒有變化,但它至少有一個子代需要佈局。dirty就是自己都變化了。

3.2 全局佈局和增量佈局

  • 全局佈局:指觸發了整個呈現樹範圍的佈局,呈現器的全局樣式更改或者屏幕大小調整都會觸發全局佈局。
  • 增量佈局:採用增量方式,也就是隻對 dirty 呈現器進行佈局(這樣可能存在需要進行額外佈局的弊端)。

當呈現器爲 dirty 時,會異步觸發增量佈局。例如,當來自網絡的額外內容添加到 DOM 樹之後,新的呈現器附加到了呈現樹中。

3.3 異步佈局和同步佈局

增量佈局是異步執行的。Firefox 將增量佈局的“reflow 命令”加入隊列,而調度程序會觸發這些命令的批量執行。WebKit 也有用於執行增量佈局的計時器:對呈現樹進行遍歷,並對 dirty 呈現器進行佈局。 請求樣式信息(例如“offsetHeight”)的腳本可同步觸發增量佈局。 全局佈局往往是同步觸發的。 有時,當初始佈局完成之後,如果一些屬性(如滾動位置)發生變化,佈局就會作爲回調而觸發。

瀏覽器的自身優化

如果佈局是由“大小調整”或呈現器的位置(而非大小)改變而觸發的,那麼可以從緩存中獲取呈現器的大小,而無需重新計算。 在某些情況下,只有一個子樹進行了修改,因此無需從根節點開始佈局。這適用於在本地進行更改而不影響周圍元素的情況,例如在文本字段中插入文本(否則每次鍵盤輸入都將觸發從根節點開始的佈局)。

因爲這個優化方案,所以你每改一次樣式,它就不會reflow或repaint一次。但是有些情況,如果我們的程序需要某些特殊的值,那麼瀏覽器需要返回最新的值,而會有一些樣式的改變,從而造成頻繁的reflow/repaint。比如獲取下面這些值,瀏覽器會馬上進行reflow:

offsetTop, offsetLeft, offsetWidth, offsetHeight scrollTop/Left/Width/Height clientTop/Left/Width/Height getComputedStyle(), currentStyle

我們可以做的性能優化

大家倒背如流的老話,再囉嗦一遍:儘量減少重繪重排。具體:

  1. 不要一條一條地修改DOM的樣式(用class批量操作)
  2. 緩存dom節點,供後面使用(for循環,取html集合長度,你懂的)
  3. 把DOM離線後修改(documentFragment、虛擬dom、把它display:none再改再顯示)
  4. 儘量修改層級比較低的DOM
  5. 有動畫的DOM使用fixed或absoult的position,脫離文檔流

4. 重繪與重排(重要)

4.1 重排(reflow)

重排(也叫回流)會計算頁面佈局(Layout)。某個節點Reflow時會重新計算節點的尺寸和位置,而且還有可能觸其後代節點reflow。重排後,瀏覽器會重新繪製受影響的部分到屏幕,該過程稱爲重繪。另外,DOM變化不一定都會影響幾何屬性,比如改變一個元素的背景色不影響寬高,這種情況下只會發生重繪,代價較小。

當DOM的變化影響了元素的幾何屬性(寬或高),瀏覽器需要重新計算元素的幾何屬性,由於流式佈局其他元素的幾何屬性和位置也受到影響。瀏覽器會使渲染樹中受到影響的部分失效,並重新構造渲染樹。 reflow 會從根節點開始遞歸往下,依次計算所有的結點幾何尺寸和位置,在reflow過程中,可能會增加一些frame,如文本字符串。DOM 樹裏的每個結點都會有reflow方法,一個結點的reflow很有可能導致子結點,甚至父點以及同級結點的reflow。

當渲染樹的一部分(或全部)因爲元素的尺寸、佈局、隱藏等改變而需要重新構建。所以,每個頁面至少需要一次reflow,就是頁面第一次加載的時候。

4.2 重繪(repaint)

repaint(重繪)遍歷所有節點,檢測節點的可見性、顏色、輪廓等可見的樣式屬性,然後根據檢測的結果更新頁面的響應部分。當渲染樹中的一些元素需要更新一些不會改變元素不局的屬性,比如只是影響元素的外觀、風格、而不會影響佈局的那些屬性,這時候就只發生重繪。當然,頁面首次加載也是要重繪一次的。

光柵:光柵主要是針對圖形的一個柵格化過程。現代瀏覽器中主要的繪製工作主要用光柵化軟件來完成。所以元素重繪由這個元素和繪製層級的關係,來決定的是否會很大程度影響你的性能-,如果這個元素蓋住的多層元素都被重新繪製,性能損耗當然大。

5. paint(繪製)

在繪製階段,系統會遍歷渲染樹,並調用呈現器的“paint”方法,將呈現器的內容繪製成位圖。繪製工作是使用用戶界面基礎組件完成的 你所看見的一切都會觸發paint。包括拖動滾動條,鼠標選擇中文字等這些完全不改變樣式,只改變顯示結果的動作都會觸發paint。paint的工作就是把文檔中用戶可見的那一部分展現給用戶。paint是把layout和樣式計算的結果直接在瀏覽器視窗上繪製出來,它並不實現具體的元素計算,只是layout後面的那一步。

繪製順序:背景顏色->背景圖片->邊框->子代->輪廓

其實就是元素進入堆棧樣式上下文的順序。這些堆棧會從後往前繪製,因此這樣的順序會影響繪製。

再說回來,在樣式發生變化時,瀏覽器會儘可能做出最小的響應。因此,元素的顏色改變後,只會對該元素進行重繪。元素的位置改變後,只會對該元素及其子元素(可能還有同級元素)進行佈局和重繪。添加 DOM 節點後,會對該節點進行佈局和重繪。一些重大變化(例如增大“html”元素的字體)會導致緩存無效,使得整個渲染樹都會進行重新佈局和繪製。

6. composite(重要)

概念不復雜,即是渲染層合併,我們將渲染樹繪製後,形成一個個圖層,最後把它們組合起來顯示到屏幕。渲染層合併。前面也說過,對於頁面中DOM元素的繪製是在多個層上進行的。在每個層上完成繪製過程之後,瀏覽器會將繪製的位圖發送給GPU繪製到屏幕上,將所有層按照合理的順序合併成一個圖層,然後在屏幕上呈現。

對於有位置重疊的元素的頁面,這個過程尤其重要,因爲一量圖層的合併順序出錯,將會導致元素顯示異常。另外,這部分主要的是這涉及到我們常說的GPU加速的問題。

說到性能優化,針對頁面渲染過程的話,我們希望的是代價最小,避免多餘的性能損失,少一點讓瀏覽器做的步驟。比如我們可以分析一下開頭的那幅圖:

明顯,我們改的越深,代價越大,所以我們只改最後一個流程——合成的時候,性能是最好的。瀏覽器會爲使用了transform或者animation的元素單獨創建一個層。當有單獨的層之後,此元素的Repaint操作將只需要更新自己,不用影響到別,局部更新。所以開啓了硬件加速的動畫會變得流暢很多。

因爲每個頁面元素都有一個獨立的渲染進程,包含了主線程和合成線程,主線程負責js的執行、CSS樣式計算、計算Layout、將頁面元素繪製成位圖(Paint)、發送位圖給合成線程。合成線程則主要負責將位圖發送給GPU、計算頁面的可見部分和即將可見部分(滾動)、通知GPU繪製位圖到屏幕上。加上一個點,GPU對於動畫圖形的渲染處理比CPU要快,那麼就可以達到加速的效果。

注意不能濫用GPU加速,一定要分析其實際性能表現。因爲GPU加速創建渲染層是有代價的,每創建一個新的渲染層,就意味着新的內存分配和更復雜的層的管理。並且在移動端 GPU 和 CPU 的帶寬有限制,創建的渲染層過多時,合成也會消耗跟多的時間,隨之而來的就是耗電更多,內存佔用更多。過多的渲染層來帶的開銷而對頁面渲染性能產生的影響,甚至遠遠超過了它在性能改善上帶來的好處。

7. 瀏覽器加載的時間線(重要)

這是補充前面的html解析爲dom部分的內容。

  1. 創建document對象,解析html,將元素對象和文本內容添加到文檔中,此時document.readyState = 'loading'
  2. 遇到link外部css的時候,創建新的線程異步加載,繼續解析html
  3. 遇到有src的scripts(沒有async和defer標記)加載外部的js時,同步加載並阻塞解析html,而且加載完馬上執行
  4. 遇到設置async和defer的script,創建新的線程異步加載,繼續解析html。async加載完馬上執行,defer在DOMContentLoaded前執行
  5. 遇到帶有src的img,解析dom結構,再異步加載src的圖片資源,不會等待img加載完成繼續解析文檔。另外,img要等待css加載完才解碼,所以css阻塞圖片的呈現,類似於js阻塞html解析一樣。可以想一下,如果css被設置爲display:none,還有意義嗎?所以此時雖然對後臺有請求但不解碼
  6. 文檔解析完畢,document.readyState = 'interactive'
  7. 此時帶有defer的js開始按順序執行
  8. DOMContentLoaded觸發,程序從同步腳本執行轉化爲事件驅動階段(類似ele.onclick = handel已經開始生效)
  9. 當所有的script加載完成並且成功執行、img和css加載完畢,document.readyState = 'completed',觸發onload事件
  10. 異步響應ui行爲,開始交互

補充:script和link標籤的問題

明顯,CSSOM樹和DOM樹是互不關聯的兩個過程。平時我們把link標籤放部頭而script放body尾部,因爲js阻塞阻塞DOM樹的構建。但是js需要查詢CSS信息,所以js還要等待CSSOM樹構建完纔可以執行。這就造成CSS阻塞了js,js阻塞了DOM樹構建。所以我們只要設置link的preload來預加載css文件,解決了js執行時CSSOM樹還沒構建好的阻塞問題。當然,script異步加載也是另外的方法。

總的來說,參考一下很多人說過的規律:

  • CSS 不會阻塞 DOM 的解析,但會阻塞 DOM 渲染。
  • JS 阻塞 DOM 解析,但瀏覽器會"偷看"DOM,提前下載資源。
  • 瀏覽器遇到 script且沒有defer或async屬性的標籤時,會觸發頁面渲染,因而如果前面CSS資源尚未加載完畢時,瀏覽器會等待它加載完畢在執行腳本。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章