身爲前端,打交道最多的就是瀏覽器和node了,也是咱們必須熟悉的。接下來咱們講一下瀏覽器工做原理和工做過程。從url到頁面的過程,......,咱們直接來到收到服務器返回內容部分開始。css
先上不少人都見過的一幅圖: html
還有一幅圖: 前端
瀏覽器主要組成部分:node
body
元素的width變化會影響其後代元素的寬度。所以,佈局過程是常常發生的。詞法分析器將輸入內容分解成一個個有效標記,解析器負責根據語言的語法規則分析文檔的結構來構建解析樹。詞法分析器知道如何將無關的字符(空格、換行符等)分離出來,因此咱們平時寫一些空格也不會影響大局。git
在語法分析的過程當中,解析器會向詞法分析器請求一個標記(就是前面分解出來的標記),並嘗試將其與某條語法規則(好比標籤要閉合、正確嵌套)進行匹配。若是發現了匹配規則,解析器會將一個對應於該標記的節點添加到解析樹中,而後繼續請求下一個標記。github
若是沒有規則能夠匹配,解析器就會將標記存儲到內部,並繼續請求標記,直至找到可與全部內部存儲的標記匹配的規則(如div多層嵌套的狀況,這樣子能找到div閉合部分)。若是找不到任何匹配規則,解析器就會引起一個異常。這意味着文檔無效,包含語法錯誤。算法
解析器類型有兩種:json
編譯:將源代碼編譯成機器代碼,源代碼先走完解析的過程造成成解析樹,解析樹被翻譯成機器代碼文檔,完成編譯的過程後端
特殊的是,剛好html不能用上面兩種解析方法。有一種能夠定義 HTML 的正規格式:DTD,但它不是與上下文無關的語法,html明顯是和上下文關係緊密的。咱們知道 HTML 是有點「隨意」的,對於不閉合的或者不正確嵌套標籤有可能不報錯,而且嘗試解釋成正確的樣子,具備必定的容錯機性,所以能夠達到簡化網絡開發的效果。另外一方面,這使得它很難編寫正式的語法。歸納地說,HTML 沒法很容易地經過常規解析器解析(由於它的語法不是與上下文無關的語法),因此採用了 DTD 格式。api
解析器解析html文檔的解析樹是由 DOM 元素和屬性節點構成的樹結構。它是 HTML 文檔的對象表示,同時也是外部內容(例如 JavaScript)與 HTML 元素之間的api,其根節點是document。上面已經說到,不能使用常規的解析技術解釋html,瀏覽器就建立了自定義的解析器來解析 。對於HTML/SVG/XHTML這三種文檔,Webkit有三個C++的類對應這三種文檔,併產生一個DOM Tree。解釋html成dom的過程,由兩個階段組成:標記化和樹構建。
對於一段html:
<html>
<body>
hi
</body>
</html>
複製代碼
該算法使用狀態機來表示。每個狀態接收來自輸入信息流的一個或多個字符,並根據這些字符更新下一個狀態。當前的標記化狀態和樹結構狀態會影響進入下一狀態的決定。
初始狀態是數據狀態。遇到字符 < 時,狀態更改成「標記打開狀態」。接收一個字母會建立「起始標記」,狀態更改成「標記名稱狀態」。這個狀態會一直保持到接收 > 字符,接收到將會進入「標記打開狀態」。在此期間接收的每一個字符都會附加到新的標記名稱上。
好比咱們先寫html標籤,先遇到<,進入「標記打開狀態」,遇到html四個字母進入「標記名稱狀態」,接着接收到了>字符,會發送當前的標記,狀態改回「數據狀態」
<body>
標記也會進行一樣的處理。如今 html 和 body 標記均已發出,並且目前是「數據狀態」。接收到 hi中的 h 字符時,將建立併發送字符標記,直到接收 </body>
中的 <。咱們將爲hi的每一個字符都發送一個字符標記。
回到「標記打開狀態」。接收下一個輸入字符 / 時,會建立閉合標籤token,並改成「標記名稱狀態」。咱們會再次保持這個狀態,直到接收 >。而後將發送新的標記,並回到「數據狀態」。最後,</html>
輸入也會進行一樣的處理。
在建立解析器的同時也會建立 document 對象。在樹構建階段,以 Document 爲根節點的 DOM 樹也會不斷進行修改,向其中添加各類元素。標記生成器發送的每一個節點都會由樹構建器進行處理。
樹構建階段的輸入是一個來自標記化階段的標記序列。第一個模式是「initial mode」。接收 HTML 標記後轉爲「before html」模式,並在這個模式下從新處理此標記。這樣會建立一個 HTMLHtmlElement 元素,並將其附加到 Document 根對象上。
狀態改成「before head」。此時咱們接收「body」標記。因爲容錯性,就算咱們的沒head標籤,系統也會隱式建立一個 HTMLHeadElement,並將其添加到樹中。
進入了「in head」模式,而後轉入「after head」模式。系統對 body 標記進行從新處理,建立並插入 HTMLBodyElement,同時模式轉變爲「in body」。
接收由「hi」字符串生成的一系列字符標記。接收第一個字符時會建立並插入文本節點,而其餘字符也將附加到該節點。固然還有其餘節點,好比屬性節點、換行節點。咱們實際場景還有外部資源以及其餘各類各樣的複雜標籤嵌套和內容結構,不過原理都相似。對於中間這個過程,遇到外部資源如何處理,順序是怎樣的,後面再講。
接收 body 結束標記會觸發「after body」模式。如今咱們將接收 HTML 結束標記,而後進入「after after body」模式。接收到文件結束標記後,解析過程就此結束,dom樹已經創建完畢(不是加載完畢,在DOMContentLoaded以前,document.readyState = ‘interactive ’)。
結束後,此時文檔被標註爲交互狀態,瀏覽器開始解析那些script標籤上帶有「defer」腳本,也就是那些應在文檔解析完成後才執行的腳本,文檔狀態將設置爲「完成」,執行完畢觸發DOMContentLoaded事件(當初始的 HTML 文檔被徹底加載和解析完成以後,DOMContentLoaded 事件被觸發,不會等待樣式表、圖像和iframe的完成加載)。
解析CSS會產生CSS規則樹,前面已經說到,html不是與上下文無關的語法,而css和js是與上下文無關的語法,因此常規的解析方法均可以用。對於創建CSS 規則樹,是須要比照着DOM樹來的。CSS匹配DOM樹主要是從右到左解析CSS選擇器。解析CSS的順序是瀏覽器的樣式 -> 用戶自定義的樣式 -> 頁面的link標籤等引進來的樣式 -> 寫在style標籤裏面的內聯樣式
樣式表不會更改 DOM 樹,所以沒有必要等待樣式表並中止文檔解析。而腳本在文檔解析階段會請求樣式信息時尚未加載和解析樣式,腳本就會得到錯誤的回覆。Firefox 在樣式表加載和解析的過程當中,會禁止全部腳本。而對於 WebKit 而言,僅當腳本嘗試訪問的樣式屬性可能受還沒有加載的樣式表影響時,它纔會禁止該腳本。
網絡整個解析的過程是同步的,會暫停 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)。固然你能夠寫不少代碼,不過通常沒見過有人這麼幹。
html、css、js解析完成後,瀏覽器引擎會經過DOM Tree 和 CSS Rule Tree 來構造 Rendering Tree(渲染樹)。
有一些 DOM 元素對應多個可視化對象。它們每每是具備複雜結構的元素,沒法用單一的矩形來描述。如「select」元素有 3 個呈現器:一個用於顯示區域,一個用於下拉列表框,還有一個用於按鈕。若是因爲寬度不夠,文本沒法在一行中顯示而分爲多行,那麼新的行也會做爲新的呈現器而添加。
inline 元素只能包含 block 元素或 inline 元素中的一種。若是出現了混合內容,則應建立匿名的 block 呈現器,以包裹 inline 元素。因此咱們平時的inline-block能夠設置寬高。
有一些呈現對象對應於 DOM 節點,但在樹中所在的位置與 DOM 節點不一樣。脫離文檔流的浮動定位和絕對定位的元素就是這樣,被放置在樹中的其餘地方,並映射到真正的frame,而放在原位的是佔位frame。
構建渲染樹以前,須要計算每個呈現對象的可視化屬性。這是經過計算每一個元素的樣式屬性來完成的。
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也是。
Firefox:系統會針對 DOM 更新註冊展現層,做爲偵聽器。展現層將框架建立工做委託FrameConstructor,由該構造器解析樣式並建立frame。
WebKit:解析樣式和建立呈現器的過程稱爲「附加」。每一個 DOM 節點都有一個「attach」方法。附加是同步進行的,將節點插入 DOM 樹須要調用新的節點「attach」方法。
處理 html 和 body 標記就會構建渲染樹根節點。這個根節點呈現對象對應於 CSS 規範中所說的容器 block,這是最上層的 block,包含了其餘全部 block。它的尺寸就是視口,即瀏覽器窗口顯示區域的尺寸。Firefox 稱之爲 ViewPortFrame,而 WebKit 稱之爲 RenderView。這就是文檔所指向的呈現對象。渲染樹的其他部分以 DOM 樹節點插入的形式來構建。
呈現器在建立完成並添加到渲染樹時,並不包含位置和大小信息。**計算這些值的過程**稱爲佈局(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),最後激發渲染層合併過程,生成最後的畫面。因爲元素相覆蓋,相互影響,稍有不慎的操做就有可能致使一次自上而下的佈局計算。因此咱們在進行元素操做的時候要一再當心儘可能避免修改這些從新佈局的屬性。
若是呈現器在佈局過程當中須要換行,會當即中止佈局,並告知其父代須要換行。父代會建立額外的呈現器,並對其調用佈局。
爲避免對全部細小更改都進行總體佈局,瀏覽器採用了一種「dirty 位」系統。若是某個呈現器發生了更改,或者將自身及其子代標註爲「dirty」,則須要進行佈局。相似於髒檢測。
有「dirty」和「children are dirty」兩種標記方法。「children are dirty」表示儘管呈現器自身沒有變化,但它至少有一個子代須要佈局。dirty就是本身都變化了。
當呈現器爲 dirty 時,會異步觸發增量佈局。例如,當來自網絡的額外內容添加到 DOM 樹以後,新的呈現器附加到了呈現樹中。
增量佈局是異步執行的。Firefox 將增量佈局的「reflow 命令」加入隊列,而調度程序會觸發這些命令的批量執行。WebKit 也有用於執行增量佈局的計時器:對呈現樹進行遍歷,並對 dirty 呈現器進行佈局。 請求樣式信息(例如「offsetHeight」)的腳本可同步觸發增量佈局。 全局佈局每每是同步觸發的。 有時,當初始佈局完成以後,若是一些屬性(如滾動位置)發生變化,佈局就會做爲回調而觸發。
若是佈局是由「大小調整」或呈現器的位置(而非大小)改變而觸發的,那麼能夠從緩存中獲取呈現器的大小,而無需從新計算。 在某些狀況下,只有一個子樹進行了修改,所以無需從根節點開始佈局。這適用於在本地進行更改而不影響周圍元素的狀況,例如在文本字段中插入文本(不然每次鍵盤輸入都將觸發從根節點開始的佈局)。
由於這個優化方案,因此你每改一次樣式,它就不會reflow或repaint一次。可是有些狀況,若是咱們的程序須要某些特殊的值,那麼瀏覽器須要返回最新的值,而會有一些樣式的改變,從而形成頻繁的reflow/repaint。好比獲取下面這些值,瀏覽器會立刻進行reflow:
offsetTop, offsetLeft, offsetWidth, offsetHeight scrollTop/Left/Width/Height clientTop/Left/Width/Height getComputedStyle(), currentStyle
你們滾瓜爛熟的老話,再囉嗦一遍:儘可能減小重繪重排。具體:
重排(也叫回流)會計算頁面佈局(Layout)。某個節點Reflow時會從新計算節點的尺寸和位置,並且還有可能觸其後代節點reflow。重排後,瀏覽器會從新繪製受影響的部分到屏幕,該過程稱爲重繪。另外,DOM變化不必定都會影響幾何屬性,好比改變一個元素的背景色不影響寬高,這種狀況下只會發生重繪,代價較小。
當DOM的變化影響了元素的幾何屬性(寬或高),瀏覽器須要從新計算元素的幾何屬性,因爲流式佈局其餘元素的幾何屬性和位置也受到影響。瀏覽器會使渲染樹中受到影響的部分失效,並從新構造渲染樹。 reflow 會從根節點開始遞歸往下,依次計算全部的結點幾何尺寸和位置,在reflow過程當中,可能會增長一些frame,如文本字符串。DOM 樹裏的每一個結點都會有reflow方法,一個結點的reflow頗有可能致使子結點,甚至父點以及同級結點的reflow。
當渲染樹的一部分(或所有)由於元素的尺寸、佈局、隱藏等改變而須要從新構建。因此,每一個頁面至少須要一次reflow,就是頁面第一次加載的時候。
repaint(重繪)遍歷全部節點,檢測節點的可見性、顏色、輪廓等可見的樣式屬性,而後根據檢測的結果更新頁面的響應部分。當渲染樹中的一些元素須要更新一些不會改變元素不局的屬性,好比只是影響元素的外觀、風格、而不會影響佈局的那些屬性,這時候就只發生重繪。固然,頁面首次加載也是要重繪一次的。
光柵:光柵主要是針對圖形的一個柵格化過程。現代瀏覽器中主要的繪製工做主要用光柵化軟件來完成。因此元素重繪由這個元素和繪製層級的關係,來決定的是否會很大程度影響你的性能-,若是這個元素蓋住的多層元素都被從新繪製,性能損耗固然大。
在繪製階段,系統會遍歷渲染樹,並調用呈現器的「paint」方法,將呈現器的內容繪製成位圖。繪製工做是使用用戶界面基礎組件完成的 你所看見的一切都會觸發paint。包括拖動滾動條,鼠標選擇中文字等這些徹底不改變樣式,只改變顯示結果的動做都會觸發paint。paint的工做就是把文檔中用戶可見的那一部分展示給用戶。paint是把layout和樣式計算的結果直接在瀏覽器視窗上繪製出來,它並不實現具體的元素計算,只是layout後面的那一步。
繪製順序:背景顏色->背景圖片->邊框->子代->輪廓
其實就是元素進入堆棧樣式上下文的順序。這些堆棧會從後往前繪製,所以這樣的順序會影響繪製。
再說回來,在樣式發生變化時,瀏覽器會盡量作出最小的響應。所以,元素的顏色改變後,只會對該元素進行重繪。元素的位置改變後,只會對該元素及其子元素(可能還有同級元素)進行佈局和重繪。添加 DOM 節點後,會對該節點進行佈局和重繪。一些重大變化(例如增大「html」元素的字體)會致使緩存無效,使得整個渲染樹都會進行從新佈局和繪製。
概念不復雜,便是渲染層合併,咱們將渲染樹繪製後,造成一個個圖層,最後把它們組合起來顯示到屏幕。渲染層合併。前面也說過,對於頁面中DOM元素的繪製是在多個層上進行的。在每一個層上完成繪製過程以後,瀏覽器會將繪製的位圖發送給GPU繪製到屏幕上,將全部層按照合理的順序合併成一個圖層,而後在屏幕上呈現。
對於有位置重疊的元素的頁面,這個過程尤爲重要,由於一量圖層的合併順序出錯,將會致使元素顯示異常。另外,這部分主要的是這涉及到咱們常說的GPU加速的問題。
說到性能優化,針對頁面渲染過程的話,咱們但願的是代價最小,避免多餘的性能損失,少一點讓瀏覽器作的步驟。好比咱們能夠分析一下開頭的那幅圖:
明顯,咱們改的越深,代價越大,因此咱們只改最後一個流程——合成的時候,性能是最好的。瀏覽器會爲使用了transform或者animation的元素單首創建一個層。當有單獨的層以後,此元素的Repaint操做將只須要更新本身,不用影響到別,局部更新。因此開啓了硬件加速的動畫會變得流暢不少。
由於每一個頁面元素都有一個獨立的渲染進程,包含了主線程和合成線程,主線程負責js的執行、CSS樣式計算、計算Layout、將頁面元素繪製成位圖(Paint)、發送位圖給合成線程。合成線程則主要負責將位圖發送給GPU、計算頁面的可見部分和即將可見部分(滾動)、通知GPU繪製位圖到屏幕上。加上一個點,GPU對於動畫圖形的渲染處理比CPU要快,那麼就能夠達到加速的效果。
注意不能濫用GPU加速,必定要分析其實際性能表現。由於GPU加速建立渲染層是有代價的,每建立一個新的渲染層,就意味着新的內存分配和更復雜的層的管理。而且在移動端 GPU 和 CPU 的帶寬有限制,建立的渲染層過多時,合成也會消耗跟多的時間,隨之而來的就是耗電更多,內存佔用更多。過多的渲染層來帶的開銷而對頁面渲染性能產生的影響,甚至遠遠超過了它在性能改善上帶來的好處。
這是補充前面的html解析爲dom部分的內容。
明顯,CSSOM樹和DOM樹是互不關聯的兩個過程。平時咱們把link標籤放部頭而script放body尾部,由於js阻塞阻塞DOM樹的構建。可是js須要查詢CSS信息,因此js還要等待CSSOM樹構建完才能夠執行。這就形成CSS阻塞了js,js阻塞了DOM樹構建。因此咱們只要設置link的preload來預加載css文件,解決了js執行時CSSOM樹還沒構建好的阻塞問題。固然,script異步加載也是另外的方法。
總的來講,參考一下不少人說過的規律: