轉載自web fundamentalcss
瀏覽器渲染頁面前須要先構建 DOM 和 CSSOM 樹。所以,咱們須要確保儘快將 HTML 和 CSS 都提供給瀏覽器。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>
一個包含一些文本和一幅圖片的普通 HTML 頁面,瀏覽器如何處理此頁面?html5
HTML解析器輸出的樹是由DOM元素和屬性節點組成的,它是HTML文檔的對象化描述,也是HTML元素與外界(如Javascript)的接口。DOM與標籤有着幾乎一一對應的關係。node
整個流程最終輸出是頁面的文檔對象模型 (DOM),瀏覽器對頁面進行的全部進一步處理都會用到它。web
瀏覽器每次處理 HTML 標記時,都會完成以上全部步驟:將字節轉換成字符,肯定tokens,將tokens轉換成節點,而後構建 DOM 樹。這整個流程可能須要一些時間才能完成,有大量 HTML 須要處理時更是如此。瀏覽器
若是您打開 Chrome DevTools 並在頁面加載時記錄時間線,就能夠看到執行該步驟實際花費的時間。在上例中,將一堆 HTML 字節轉換成 DOM 樹大約須要 5 毫秒。對於較大的頁面,這一過程須要的時間可能會顯著增長。建立流暢動畫時,若是瀏覽器須要處理大量 HTML,這很容易成爲瓶頸。緩存
DOM 樹捕獲文檔標記的屬性和關係,但並未告訴咱們元素在渲染後呈現的外觀。那是 CSSOM 的責任。服務器
在瀏覽器構建這個簡單頁面的 DOM 過程當中,在文檔的 head 中遇到了一個 link 標記,該標記引用一個外部 CSS 樣式表:style.css。因爲預見到須要利用該資源來渲染頁面,它會當即發出對該資源的請求,並返回如下內容:網絡
body { font-size: 16px } p { font-weight: bold } span { color: red } p span { display: none } img { float: right }
咱們本能夠直接在 HTML 標記內聲明樣式(內聯),但讓 CSS 獨立於 HTML 有利於咱們將內容和設計做爲獨立關注點進行處理:設計人員負責處理 CSS,開發者側重於 HTML,等等。數據結構
與處理 HTML 時同樣,咱們須要將收到的 CSS 規則轉換成某種瀏覽器可以理解和處理的東西。所以,咱們會重複 HTML 過程,不過是爲 CSS 而不是 HTML:
CSS 字節轉換成字符,接着轉換成tokens和節點,最後連接到一個稱爲「CSS 對象模型」(CSSOM) 的樹結構:
CSSOM 爲什麼具備樹結構?爲頁面上的任何節點對象計算最後一組樣式時,瀏覽器都會先從適用於該節點的最通用規則開始(例如,若是該節點是 body 元素的子元素,則應用全部 body 樣式),而後經過應用更具體的規則以遞歸方式優化計算的樣式。
以上面的 CSSOM 樹爲例進行更具體的闡述。任何置於 body 元素內span 標記中的文本都將具備 16 像素字號,而且顏色爲紅色 。font-size 指令從 body 向下級層疊至 span。不過,若是某個 span 標記是某個段落 (p) 標記的子項,則其內容將不會顯示。
Also, note that the above tree is not the complete CSSOM tree and only shows the styles we decided to override in our stylesheet.每一個瀏覽器都提供一組默認樣式(也稱爲「User Agent 樣式」),即咱們的樣式只是override這些默認樣式。
要了解 CSS 處理所需的時間,能夠在 DevTools 中記錄時間線並尋找「Recalculate Style」事件:unlike DOM parsing, the timeline doesn’t show a separate "Parse CSS" entry, and instead captures parsing and CSSOM tree construction, plus the recursive calculation of computed styles under this one event.
咱們的小樣式表須要大約 0.6 毫秒的處理時間,影響頁面上的 8 個元素 — 雖然很少,但一樣會產生開銷。不過,這 8 個元素從何而來呢?將 DOM 與 CSSOM 關聯在一塊兒的是渲染樹。
CSSOM 樹和 DOM 樹合併成渲染樹,而後用於計算每一個可見元素的佈局,並輸出給繪製流程,將像素渲染到屏幕上。優化上述每個步驟對實現最佳渲染性能相當重要。
瀏覽器根據 HTML 和 CSS 輸入構建了 DOM 樹和 CSSOM 樹。 不過,它們是彼此徹底獨立的對象,分別capture文檔不一樣方面的信息:一個描述內容,另外一個則是描述須要對文檔應用的樣式規則。咱們該如何將二者合併,讓瀏覽器在屏幕上渲染像素呢?
第一步是讓瀏覽器將 DOM 和 CSSOM 合併成一個「渲染樹」,網羅網頁上全部可見的 DOM 內容,以及每一個節點的全部 CSSOM 樣式信息。
爲構建渲染樹,瀏覽器大致上完成了下列工做:
注: visibility: hidden 與 display: none 是不同的。前者隱藏元素,但元素仍佔據着佈局空間(即將其渲染成一個空框),然後者 (display: none) 將元素從渲染樹中徹底移除,元素既不可見,也不是佈局的組成部分。
最終輸出的渲染同時包含了屏幕上的全部可見內容及其樣式信息。有了渲染樹,咱們就能夠進入「佈局」階段。
到目前爲止,咱們計算了哪些節點應該是可見的以及它們的計算樣式,但咱們還沒有計算它們在設備視口內的確切位置和大小---這就是「佈局」階段,也稱爲「reflow」。
爲弄清每一個對象在網頁上的確切大小和位置,瀏覽器從渲染樹的根節點開始進行遍歷。讓咱們考慮一個簡單的實例:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Critial Path: Hello world!</title> </head> <body> <div style="width: 50%"> <div style="width: 50%">Hello world!</div> </div> </body> </html>
以上網頁的正文包含兩個嵌套 div:第一個(父)div 將節點的顯示尺寸設置爲視口寬度的 50%,父 div 包含的第二個 div寬度爲其父項的 50%,即視口寬度的 25%。
佈局流程的輸出是一個「盒模型」,它會精確地捕獲每一個元素在視口內的確切位置和尺寸:全部相對測量值都轉換爲屏幕上的絕對像素。
最後,既然咱們知道了哪些節點可見、它們的computed styles以及幾何信息,咱們終於能夠將這些信息傳遞給最後一個階段:將渲染樹中的每一個節點轉換成屏幕上的實際像素。這一步一般稱爲"painting" or "rasterizing."。
Chrome DevTools 能夠幫助咱們對上述全部三個階段的耗時進行深刻的瞭解。讓咱們看一下最初「hello world」示例的佈局階段:
The "Layout" event captures the render tree construction, position, and size calculation in the Timeline.
When layout is complete, the browser issues "Paint Setup" and "Paint" events, which convert the render tree to pixels on the screen.
執行渲染樹構建、佈局和繪製所需的時間將取決於文檔大小、應用的樣式,以及運行文檔的設備:文檔越大,瀏覽器須要完成的工做就越多;樣式越複雜,繪製須要的時間就越長(例如,單色的繪製開銷「較小」,而陰影的計算和渲染開銷則要「大得多」)。
下面簡要概述了瀏覽器完成的步驟:
若是 DOM 或 CSSOM 被修改,須要再執行一遍以上全部步驟,以肯定哪些像素須要在屏幕上進行從新渲染。
Optimizing the critical rendering path is the process of minimizing the total amount of time spent performing steps 1 through 5 in the above sequence. Doing so renders content to the screen as quickly as possible and also reduces the amount of time between screen updates after the initial render; that is, achieve higher refresh rates for interactive content.
默認狀況下,CSS 被視爲阻塞渲染的資源(但不阻塞html的解析),這意味着瀏覽器將不會渲染任何已處理的內容,直至 CSSOM 構建完畢。請務必精簡CSS,儘快提供它,並利用媒體類型和查詢來解除對渲染的阻塞,以縮短首屏的時間。
在渲染樹構建中,要求同時具備 DOM 和 CSSOM 才能構建渲染樹。這會給性能形成嚴重影響:HTML 和 CSS 都是阻塞渲染的資源。 HTML 顯然是必需的,由於若是沒有 DOM,就沒有可渲染的內容,但 CSS 的必要性可能就不太明顯。若是在 CSS 不阻塞渲染的狀況下嘗試渲染一個普通網頁會怎樣?
沒有 CSS 的網頁實際上沒法使用。因此瀏覽器將阻塞渲染,直至 DOM 和 CSSOM 全都準備就緒。
CSS 是阻塞渲染的資源。須要將它儘早、儘快地下載到客戶端,以便縮短首次渲染的時間。
若是有一些 CSS 樣式只在特定條件下(例如顯示網頁或將網頁投影到大型顯示器上時)使用,又該如何?若是這些資源不阻塞渲染,該有多好。
能夠經過 CSS「媒體類型」和「媒體查詢」來解決這類狀況:
<link href="style.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 40em)">
媒體查詢由媒體類型以及零個或多個檢查特定媒體特徵情況的表達式組成。例如,第一個樣式表聲明未提供任何媒體類型或查詢,所以它適用於全部狀況。也就是說它始終會阻塞渲染。第二個樣式表則否則,它只在打印內容時適用---或許您想從新安排佈局、更改字體等等,所以在網頁首次加載時,該樣式表不須要阻塞渲染。最後一個樣式表聲明提供了由瀏覽器執行的「媒體查詢」:符合條件時,樣式表會生效,瀏覽器將阻塞渲染,直至樣式表下載並處理完畢。
經過使用媒體查詢,咱們能夠根據特定用例(好比顯示或打印),也能夠根據動態狀況(好比屏幕方向變化、尺寸調整事件等)定製外觀。聲明樣式表時,請密切注意媒體類型和查詢,由於它們將嚴重影響關鍵渲染路徑的性能。
讓咱們考慮下面這些實例:
<link href="style.css" rel="stylesheet">
<link href="style.css" rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<link href="print.css" rel="stylesheet" media="print">
最後,「阻塞渲染」僅是指瀏覽器是否須要暫停網頁的首次渲染,直至該資源準備就緒。不管媒尋是否命中,瀏覽器都會下載上述全部的CSS樣式表,只不過不阻塞渲染的資源對當前媒體不生效罷了。
JavaScript 容許咱們修改網頁的方方面面:內容、樣式以及它如何響應用戶交互。不過,JavaScript 也會阻止 DOM 構建和延緩網頁渲染。爲了實現最佳性能,可讓 JavaScript 異步執行,並去除關鍵渲染路徑中任何沒必要要的 JavaScript。
JavaScript 是一種運行在瀏覽器中的動態語言,它容許對網頁行爲的幾乎每個方面進行修改:能夠經過在 DOM 樹中添加和移除元素來修改內容;能夠修改每一個元素的 CSSOM 屬性;能夠處理用戶輸入等等。爲進行說明,讓咱們用一個簡單的內聯腳本對以前的「Hello World」示例進行擴展:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> <title>Critical Path: Script</title> <style> body { font-size: 16px };p { font-weight: bold }; span { color: red };p span { display: none }; img { float: right }</style> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script> var span = document.getElementsByTagName('span')[0]; span.textContent = 'interactive'; // change DOM text content span.style.display = 'inline'; // change CSSOM property // create a new element, style it, and append it to the DOM var loadTime = document.createElement('div'); loadTime.textContent = 'You loaded this page on: ' + new Date(); loadTime.style.color = 'blue'; document.body.appendChild(loadTime); </script> </body> </html>
儘管 JavaScript 爲咱們帶來了許多功能,不過也在頁面渲染方式和時間方面施加了更多限制。
首先,請注意上例中的內聯腳本靠近網頁底部。爲何呢?若是咱們將腳本移至 span元素前面,就會腳本運行失敗,並提示在文檔中找不到對任何span 元素的引用 -- 即 getElementsByTagName(‘span') 會返回 null。這透露出一個重要事實:腳本在文檔的何處插入,就在何處執行。當 HTML 解析器遇到一個 script 標記時,它會暫停構建 DOM,將控制權移交給 JavaScript 引擎;等 JavaScript 引擎運行完畢,瀏覽器會從中斷的地方恢復 DOM 構建。
換言之,咱們的腳本塊在運行時找不到網頁中任何靠後的元素,由於它們還沒有被處理!或者說:執行內聯腳本會阻止 DOM 構建,也就延緩了首次渲染。
在網頁中引入腳本的另外一個微妙事實是,它們不只能夠讀取和修改 DOM 屬性,還能夠讀取和修改 CSSOM 屬性。實際上,示例中就是這麼作的:將 span 元素的 display 屬性從 none 更改成 inline。最終結果如何?咱們如今遇到了race condition(資源競爭)。
若是瀏覽器還沒有完成 CSSOM 的下載和構建,而卻想在此時運行腳本,會怎樣?答案很簡單,對性能不利:瀏覽器將延遲腳本執行和 DOM 構建,直至其完成 CSSOM 的下載和構建。
簡言之,JavaScript 在 DOM、CSSOM 和 JavaScript 執行之間引入了大量新的依賴關係,從而可能致使瀏覽器在處理以及在屏幕上渲染網頁時出現大幅延遲:
「優化關鍵渲染路徑」在很大程度上是指了解和優化 HTML、CSS 和 JavaScript 之間的依賴關係譜。
默認狀況下,JavaScript 執行會「阻塞解析器」:當瀏覽器遇到文檔中的腳本時,它必須暫停 DOM 構建,將控制權移交給 JavaScript 運行時,讓腳本執行完畢,而後再繼續構建 DOM。實際上,內聯腳本始終會阻止解析器,除非編寫額外代碼來推遲它們的執行。
經過 script 標籤引入的腳本又怎樣:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> <title>Critical Path: Script External</title> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js"></script> </body> </html>
app.js
var span = document.getElementsByTagName('span')[0]; span.textContent = 'interactive'; // change DOM text content span.style.display = 'inline'; // change CSSOM property // create a new element, style it, and append it to the DOM var loadTime = document.createElement('div'); loadTime.textContent = 'You loaded this page on: ' + new Date(); loadTime.style.color = 'blue'; document.body.appendChild(loadTime);
不管咱們使用 <script> 標記仍是內聯 JavaScript 代碼段,二者可以以相同方式工做。 在兩種狀況下,瀏覽器都會先暫停並執行腳本,而後纔會處理剩餘文檔。若是是外部 JavaScript 文件,瀏覽器必須停下來,等待從磁盤、緩存或遠程服務器獲取腳本,這就可能給關鍵渲染路徑增長更長的延遲。
默認狀況下,全部 JavaScript 都會阻止解析器。因爲瀏覽器不瞭解腳本計劃在頁面上執行什麼操做,它會做最壞的假設並阻止解析器。向瀏覽器傳遞腳本不須要在引用位置執行的信號既可讓瀏覽器繼續構建 DOM,也可以讓腳本在就緒後執行。爲此,咱們能夠將腳本標記爲異步:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> <title>Critical Path: Script Async</title> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js" async></script> </body> </html>
向 script 標記添加異步關鍵字能夠指示瀏覽器在等待腳本可用期間(僅指下載期間,由於全部腳本的執行都會阻塞解析器)不阻止 DOM 構建,這樣能夠顯著提高性能。
發現和解決關鍵渲染路徑性能瓶頸須要充分了解常見的陷阱。讓咱們踏上實踐之旅,找出常見的性能模式,從而幫助您優化網頁。
優化關鍵渲染路徑可以讓瀏覽器儘量快地繪製網頁:更快的網頁渲染速度能夠提升吸引力、增長網頁瀏覽量以及提升轉化率。爲了最大程度減小訪客看到空白屏幕的時間,咱們須要優化加載的資源及其加載順序。
爲幫助說明這一流程,讓咱們先從可能的最簡單狀況入手,逐步構建咱們的網頁,使其包含更多資源、樣式和應用邏輯。在此過程當中,咱們還會對每一種狀況進行優化,以及瞭解可能出錯的環節。
到目前爲止,咱們只關注了資源(CSS、JS 或 HTML 文件)可供處理後瀏覽器中會發生的狀況,而忽略了從緩存或從網絡獲取資源所需的時間。咱們做如下假設:
Hello World 體驗
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Critical Path: No Style</title> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> </body> </html>
咱們將從基本 HTML 標記和單個圖像(無 CSS 或 JavaScript)開始。讓咱們在 Chrome DevTools 中打開 Network 時間線並檢查生成的資源瀑布:
正如預期的同樣,HTML 文件下載花費了大約 200 毫秒。請注意,藍線的透明部分表示瀏覽器在網絡上等待(即還沒有收到任何響應字節)的時間,而不透明部分表示的是收到第一批響應字節後完成下載的時間。HTML 下載量很小 (<4K),咱們只需單次往返即可獲取整個文件。所以,獲取 HTML 文檔大約須要 200 毫秒,其中一半的時間花費在網絡等待上,另外一半花費在等待服務器響應上。
當 HTML 內容可用後,瀏覽器會解析字節,將它們轉換成tokens,而後構建 DOM 樹。請注意,爲方便起見,DevTools 會在底部記錄 DOMContentLoaded 事件的時間(216 毫秒),該時間一樣與藍色垂直線相符。HTML 下載結束與藍色垂直線 (DOMContentLoaded) 之間的間隔是瀏覽器構建 DOM 樹所花費的時間 — 在本例中僅爲幾毫秒。
請注意,咱們的「趣照」並未阻止 domContentLoaded 事件。這證實,咱們構建渲染樹甚至繪製網頁時無需等待頁面上的每一個靜態資源:並不是全部資源都對快速提供首次繪製具備關鍵做用。事實上,當咱們談論關鍵渲染路徑時,一般談論的是 HTML 標記、CSS 和 JavaScript。圖像不會阻止頁面的首次渲染,不過,咱們固然也應該盡力確保系統儘快繪製圖像!
That said, the load event (also known as onload), is blocked on the image: DevTools reports the onload event at 335ms. Recall that the onload event marks the point at which all resources that the page requires have been downloaded and processed; at this point (the red vertical line in the waterfall), the loading spinner can stop spinning in the browser.
「Hello World experience」頁面雖然看起來簡單,但背後卻須要作不少工做。在實踐中,咱們還須要 HTML 以外的其餘資源:咱們可能須要 CSS 樣式表以及一個或多個用於爲網頁增長必定交互性的腳本。讓咱們將二者結合使用,看看效果如何:
<html> <head> <title>Critical Path: Measure Script</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body onload="measureCRP()"> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="timing.js"></script> </body> </html>
添加 JavaScript 和 CSS 以前:
添加 JavaScript 和 CSS 以後:
添加外部 CSS 和 JavaScript 文件將額外增長兩個瀑布請求,瀏覽器差很少會同時發出這兩個請求。不過,請注意,如今 domContentLoaded 事件與 onload 事件之間的時間差小多了。這是怎麼回事?
若是咱們用內聯腳本替換外部腳本會怎樣?即便直接將腳本內聯到網頁中,瀏覽器仍然沒法在構建 CSSOM 以前執行腳本。簡言之,內聯 JavaScript 也會阻止解析器。
不過,儘管內聯腳本會阻止 CSS,但這樣作是否能加快頁面渲染速度呢?讓咱們嘗試一下,看看會發生什麼。
外部 JavaScript:
內聯 JavaScript:
咱們減小了一個請求,但 onload 和 domContentLoaded 時間實際上沒有變化。爲何呢?怎麼說呢,咱們知道,這與 JavaScript 是內聯的仍是外部的並沒有關係,由於只要瀏覽器遇到 script 標記,就會進行阻止,並等到以前的css文件的 CSSOM 構建完畢。此外,在咱們的第一個示例中,瀏覽器是並行下載 CSS 和 JavaScript,而且差很少是同時完成。在此實例中,內聯 JavaScript 代碼並沒有多大意義。可是,咱們能夠經過多種策略加快網頁的渲染速度。
首先回想一下,全部內聯腳本都會阻止解析器,但對於外部腳本,能夠添加「async」關鍵字來解除對解析器的阻止。讓咱們撤消內聯,嘗試一下這種方法:
<html> <head> <title>Critical Path: Measure Async</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body onload="measureCRP()"> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script async src="timing.js"></script> </body> </html>
阻止解析器的(外部)JavaScript:
異步(外部)JavaScript:
效果好多了!解析 HTML 以後不久即會觸發 domContentLoaded 事件;瀏覽器已得知不要阻止 JavaScript,而且因爲沒有其餘阻止解析器的腳本,CSSOM 構建也可並行進行了。
或者,咱們也能夠同時內聯 CSS 和 JavaScript:
<html> <head> <title>Critical Path: Measure Inlined</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> p { font-weight: bold } span { color: red } p span { display: none } img { float: right } </style> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script> var span = document.getElementsByTagName('span')[0]; span.textContent = 'interactive'; // change DOM text content span.style.display = 'inline'; // change CSSOM property // create a new element, style it, and append it to the DOM var loadTime = document.createElement('div'); loadTime.textContent = 'You loaded this page on: ' + new Date(); loadTime.style.color = 'blue'; document.body.appendChild(loadTime); </script> </body> </html>
請注意,domContentLoaded 時間與前一示例中的時間實際上相同;只不過沒有將 JavaScript 標記爲異步,而是同時將 CSS 和 JS 內聯到網頁自己。這會使 HTML 頁面顯著增大,但好處是瀏覽器無需等待獲取任何外部資源,網頁已經內置了全部資源。
即使是很是簡單的網頁,優化關鍵渲染路徑也並不是垂手可得:須要瞭解不一樣資源之間的依賴關係圖,須要肯定哪些資源是「關鍵資源」,還必須在不一樣策略中作出選擇,找到在網頁上加入這些資源的恰當方式。這一問題不是一個解決方案可以解決的,每一個頁面都不盡相同。您須要遵循類似的流程,自行找到最佳策略。
不過,咱們能夠回過頭來,看看可否找出某些常規性能模式。
性能模式
最簡單的網頁只包括 HTML 標記;沒有 CSS,沒有 JavaScript,也沒有其餘類型的資源。要渲染此類網頁,瀏覽器須要發起請求,等待 HTML 文檔到達,對其進行解析,構建 DOM,最後將其渲染在屏幕上:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Critical Path: No Style</title> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> </body> </html>
T0 與 T1 之間的時間捕獲的是網絡和服務器處理時間。在最理想的狀況下(若是 HTML 文件較小),咱們只需一次網絡往返即可獲取整個文檔。因爲 TCP 傳輸協議工做方式的緣故,較大文件可能須要更屢次的往返。所以,在最理想的狀況下,上述網頁具備單次往返(最少)關鍵渲染路徑。
如今,咱們還以同一網頁爲例,但此次使用外部 CSS 文件:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> </body> </html>
咱們一樣須要一次網絡往返來獲取 HTML 文檔,而後檢索到的標記告訴咱們還須要 CSS 文件;這意味着,瀏覽器須要返回服務器並獲取 CSS,而後才能在屏幕上渲染網頁。所以,這個頁面至少須要兩次往返才能顯示出來。CSS 文件一樣可能須要屢次往返,所以重點在於「最少」。
讓咱們定義一下用來描述關鍵渲染路徑的詞彙:
如今,讓咱們將其與上面 HTML + CSS 示例的關鍵路徑特性對比一下:
咱們同時須要 HTML 和 CSS 來構建渲染樹。因此,HTML 和 CSS 都是關鍵資源:CSS 僅在瀏覽器獲取 HTML 文檔後纔會獲取,所以關鍵路徑長度至少爲兩次往返。兩項資源相加共計 9KB 的關鍵字節。
如今,讓咱們向組合內額外添加一個 JavaScript 文件。
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js"></script> </body> </html>
咱們添加了 app.js,它既是網頁上的外部 JavaScript 靜態資源,又是一種解析器阻止(即關鍵)資源。更糟糕的是,爲了執行 JavaScript 文件,咱們還須要進行阻塞並等待 CSSOM;由於JavaScript 能夠查詢 CSSOM,所以在下載 style.css 並構建 CSSOM 以前,瀏覽器將會暫停解析。
即使如此,若是咱們實際查看一下該網頁的「網絡瀑布」,就會注意到 CSS 和 JavaScript 請求差很少是同時發起的;瀏覽器獲取 HTML,發現兩項資源併發起兩個請求。所以,上述網頁具備如下關鍵路徑特性:
如今,咱們擁有了三項關鍵資源,關鍵字節總計達 11 KB,但咱們的關鍵路徑長度還是兩次往返,由於咱們能夠同時傳送 CSS 和 JavaScript。瞭解關鍵渲染路徑的特性意味着可以肯定哪些是關鍵資源,此外還能瞭解瀏覽器如何安排資源的獲取時間。讓咱們繼續探討示例。
在與網站開發者交流後,咱們意識到咱們在網頁上加入的 JavaScript 沒必要具備阻塞做用:網頁中的一些分析代碼和其餘代碼不須要阻止網頁的渲染。瞭解了這一點,咱們就能夠向 script 標記添加「async」屬性來解除對解析器的阻止:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js" async></script> </body> </html>
異步腳本具備如下幾個優勢:
所以,咱們優化過的網頁如今恢復到了具備兩項關鍵資源(HTML 和 CSS),最短關鍵路徑長度爲兩次往返,總關鍵字節數爲 9 KB。
最後,若是 CSS 樣式表只需用於打印,那會如何呢?
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet" media="print"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js" async></script> </body> </html>
由於 style.css 資源只用於打印,瀏覽器沒必要阻止它即可渲染網頁。因此,只要 DOM 構建完畢,瀏覽器便具備了渲染網頁所需的足夠信息。所以,該網頁只有一項關鍵資源(HTML 文檔),而且最短關鍵渲染路徑長度爲一次往返。
By default,CSS is treated as a render blocking resource, which means that the browser won't render any processed content until the CSSOM is constructed. html和css都是阻塞渲染的資源,因此要儘快構建完DOM和CSSDOM才能最快顯示首屏。可是CSS解析和HTML解析能夠並行。
當 HTML 解析器遇到一個 script 標記時,它會暫停構建 DOM,下載js文件(來源於外部/內聯/緩存),而後將控制權移交給 JavaScript 引擎(此時若在腳本引用其後的元素,會發生引用錯誤);等 JavaScript 引擎運行完畢,瀏覽器會從中斷的地方恢復 DOM 構建。也就是若是頁面有script標籤,DOMContentLoaded事件須要等待JS執行完才觸發。可是能夠將腳本標記爲異步,在下載js文件的過程當中不會阻塞DOM的構建。
defer 和 async都是異步下載js文件,但也有區別:
defer屬性只有ie支持,該屬性的腳本都是在頁面解析完畢以後執行,並且延遲腳本不必定按照前後順序執行。
async的js在下載完後會當即執行(所以腳本所執行的順序並非腳本在代碼中的順序,有可能後面出現的腳本先加載成功先執行)。
異步資源不會阻塞解析器,讓瀏覽器避免在執行腳本以前受阻於 CSSOM的構建。一般,若是腳本可使用 async 屬性,意味着它並不是首次渲染所必需,能夠考慮在首次渲染後異步加載腳本。
Race Condition
What if the browser hasn't finished downloading and building the CSSOM when we want to run our script? The answer is simple and not very good for performance: the browser delays script execution and DOM construction until it has finished downloading and constructing the CSSOM.即script標籤中的JS須要等待位於其前面的CSS加載完才執行。
HTML解析器怎麼構建DOM樹的?DOM樹和html標籤是一一對應的,在從上往下解析html時,會邊解析邊構建DOM。若是碰到外部資源(link或script)時,會進行外部資源的加載。外部資源是js時會暫停html解析,等js加載和執行完才繼續;外部資源是css時不影響html解析,但影響首屏渲染。
domContentLoaded:當初始 HTML 文檔已經完成加載和解析成DOM樹時觸發,不會等CSS文件、圖片、iframe加載完成。
load:when all resources(including images,) that the page requires have been downloaded and processed.經過動態獲取的資源和load事件無關。