瀏覽器渲染頁面前需要先構建 DOM 和 CSSOM 樹。因此,我們需要確保盡快將 HTML 和 CSS 都提供給瀏覽器。
字節 → 字符 → 標記 → 節點 → 對象模型。
HTML 標記轉換成文檔對象模型 (DOM);CSS 標記轉換成 CSS 對象模型 (CSSOM)。DOM 和 CSSOM 是獨立的數據結構。
Chrome DevTools Timeline可以捕獲和檢查 DOM 和 CSSOM 的構建和處理開銷。
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" 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 頁面,瀏覽器如何處理此頁面?
HTML解析器輸出的樹是由DOM元素和屬性節點組成的,它是HTML文檔的對象化描述,也是HTML元素與外界(如Javascript)的接口。DOM與標簽有著幾乎一一對應的關系。
轉換: 瀏覽器從磁盤或網絡讀取 HTML 的原始字節,并根據文件的指定編碼(如 UTF-8)將它們轉換成各個字符。
Tokenizing: 瀏覽器將字符串轉換成 W3C HTML5 標準規定的各種tokens,例如,“<html>”、“<body>”,以及其他尖括號內的字符串。每個token都具有特殊含義和一組規則。
詞法分析: 發出的標記轉換成定義其屬性和規則的“對象”。
DOM 構建: 最后,由于 HTML 標記定義不同標記之間的關系(一些標記包含在其他標記內),創建的對象鏈接在一個樹數據結構內,此結構也會捕獲原始標記中定義的父項-子項關系:HTML 對象是 body 對象的父項,body是paragraph對象的父項,依此類推。
整個流程最終輸出是頁面的文檔對象模型 (DOM),瀏覽器對頁面進行的所有進一步處理都會用到它。
瀏覽器每次處理 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樹中的node節點,在CSSOM規則樹中尋找當前節點的樣式,生成渲染樹。
布局計算每個對象的精確位置和大小。
最后一步是繪制,使用最終渲染樹將像素渲染到屏幕上。
第一步是讓瀏覽器將 DOM 和 CSSOM 合并成一個“渲染樹”,網羅網頁上所有可見的 DOM 內容,以及每個節點的所有 CSSOM 樣式信息。
為構建渲染樹,瀏覽器大體上完成了下列工作:
從 DOM 樹的根節點開始遍歷每個可見節點。
某些節點不可見(例如腳本標記、元標記等),因為它們不會體現在渲染輸出中,所以會被忽略。
某些節點通過 CSS 隱藏,因此在渲染樹中也會被忽略。例如 span 節點上設置了“display: none”屬性,所以也不會出現在渲染樹中。
遍歷每個可見節點,為其找到適配的 CSSOM 規則并應用它們。從選擇器的右邊往左邊開始匹配,也就是從CSSOM樹的子節點開始往父節點匹配。
Emit visible nodes with content and their computed styles.
注: 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.
執行渲染樹構建、布局和繪制所需的時間將取決于文檔大小、應用的樣式,以及運行文檔的設備:文檔越大,瀏覽器需要完成的工作就越多;樣式越復雜,繪制需要的時間就越長(例如,單色的繪制開銷“較小”,而陰影的計算和渲染開銷則要“大得多”)。
下面簡要概述了瀏覽器完成的步驟:
處理 HTML 標記并構建 DOM 樹。
處理 CSS 標記并構建 CSSOM 樹。
將 DOM 與 CSSOM 合并成一個渲染樹。
根據渲染樹來布局,以計算每個節點的幾何信息。
將各個節點繪制到屏幕上。
如果 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 被視為阻塞渲染的資源。
我們可以通過媒體類型和媒體查詢將一些 CSS 資源標記為不阻塞渲染。
瀏覽器會下載所有 CSS 資源,無論阻塞還是不阻塞。
沒有 CSS 的網頁實際上無法使用。所以瀏覽器將阻塞渲染,直至 DOM 和 CSSOM 全都準備就緒。
CSS 是阻塞渲染的資源。需要將它盡早、盡快地下載到客戶端,以便縮短首次渲染的時間。
如果有一些 CSS 樣式只在特定條件下(例如顯示網頁或將網頁投影到大型顯示器上時)使用,又該如何?如果這些資源不阻塞渲染,該有多好。
可以通過 CSS“媒體類型”和“媒體查詢”來解決這類情況:
<link href="style.css?1.1.11" rel="stylesheet">
<link href="print.css?1.1.11" rel="stylesheet" media="print">
<link href="other.css?1.1.11" rel="stylesheet" media="(min-width:
40em)">
媒體查詢由媒體類型以及零個或多個檢查特定媒體特征狀況的表達式組成。例如,第一個樣式表聲明未提供任何媒體類型或查詢,因此它適用于所有情況。也就是說它始終會阻塞渲染。第二個樣式表則不然,它只在打印內容時適用---或許您想重新安排布局、更改字體等等,因此在網頁首次加載時,該樣式表不需要阻塞渲染。最后一個樣式表聲明提供了由瀏覽器執行的“媒體查詢”:符合條件時,樣式表會生效,瀏覽器將阻塞渲染,直至樣式表下載并處理完畢。
通過使用媒體查詢,我們可以根據特定用例(比如顯示或打印),也可以根據動態情況(比如屏幕方向變化、尺寸調整事件等)定制外觀。聲明樣式表時,請密切注意媒體類型和查詢,因為它們將嚴重影響關鍵渲染路徑的性能。
讓我們考慮下面這些實例:
<link href="style.css?1.1.11" rel="stylesheet">
<link href="style.css?1.1.11" rel="stylesheet" media="all">
<link href="portrait.css?1.1.11" rel="stylesheet" media="orientation:portrait">
<link href="print.css?1.1.11" rel="stylesheet" media="print">
第一個聲明阻塞渲染,適用于所有情況。
第二個聲明同樣阻塞渲染:“all”是默認類型,和第一個聲明實際上是等效的。
第三個聲明具有動態媒體查詢,將在網頁加載時計算。根據網頁加載時設備的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。
最后一個聲明只在打印網頁時應用,因此網頁在瀏覽器中加載時,不會阻塞渲染。
最后,“阻塞渲染”僅是指瀏覽器是否需要暫停網頁的首次渲染,直至該資源準備就緒。無論媒尋是否命中,瀏覽器都會下載上述所有的CSS樣式表,只不過不阻塞渲染的資源對當前媒體不生效罷了。
JavaScript 允許我們修改網頁的方方面面:內容、樣式以及它如何響應用戶交互。不過,JavaScript 也會阻止 DOM 構建和延緩網頁渲染。為了實現最佳性能,可以讓 JavaScript 異步執行,并去除關鍵渲染路徑中任何不必要的 JavaScript。
JavaScript 可以查詢和修改 DOM 與 CSSOM。
JavaScript的 執行會阻止 CSSOM的構建,所以和CSSOM的構建是互斥的。
JavaScript blocks DOM construction unless explicitly declared as async.
JavaScript 是一種運行在瀏覽器中的動態語言,它允許對網頁行為的幾乎每一個方面進行修改:可以通過在 DOM 樹中添加和移除元素來修改內容;可以修改每個元素的 CSSOM 屬性;可以處理用戶輸入等等。為進行說明,讓我們用一個簡單的內聯腳本對之前的“Hello World”示例進行擴展:
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" 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 允許我們進入 DOM 并獲取對隱藏的 span 節點的引用 -- 該節點可能未出現在渲染樹中,卻仍然存在于 DOM 內。然后,在獲得引用后,就可以更改其文本,并將 display 樣式屬性從“none”替換為“inline”?,F在,頁面顯示“Hello interactive students!”。
JavaScript 還允許我們在 DOM 中創建、樣式化、追加和移除新元素。從技術上講,整個頁面可以是一個大的 JavaScript 文件,此文件逐一創建元素并對其進行樣式化。但是在實踐中,使用 HTML 和 CSS 要簡單得多。
盡管 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 執行之間引入了大量新的依賴關系,從而可能導致瀏覽器在處理以及在屏幕上渲染網頁時出現大幅延遲:
腳本在文檔中的位置很重要。
當瀏覽器遇到一個 script 標記時,DOM 構建將暫停,直至腳本完成執行。
JavaScript 可以查詢和修改 DOM 與 CSSOM。
JavaScript 執行將暫停,直至 CSSOM 就緒。即CSSDOM構建的優先級更高。
“優化關鍵渲染路徑”在很大程度上是指了解和優化 HTML、CSS 和 JavaScript 之間的依賴關系譜。
默認情況下,JavaScript 執行會“阻塞解析器”:當瀏覽器遇到文檔中的腳本時,它必須暫停 DOM 構建,將控制權移交給 JavaScript 運行時,讓腳本執行完畢,然后再繼續構建 DOM。實際上,內聯腳本始終會阻止解析器,除非編寫額外代碼來推遲它們的執行。
通過 script 標簽引入的腳本又怎樣:
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" 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?1.1.11"></script> </body></html>
app.js
var span = document.getElementsByTagName('span')[0]; span.textContent = 'interactive'; // change DOM text contentspan.style.display = 'inline'; // change CSSOM property// create a new element, style it, and append it to the DOMvar 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?1.1.11" 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?1.1.11" async></script> </body> </html>
向 script 標記添加異步關鍵字可以指示瀏覽器在等待腳本可用期間(僅指下載期間,因為所有腳本的執行都會阻塞解析器)不阻止 DOM 構建,這樣可以顯著提升性能。
發現和解決關鍵渲染路徑性能瓶頸需要充分了解常見的陷阱。讓我們踏上實踐之旅,找出常見的性能模式,從而幫助您優化網頁。
優化關鍵渲染路徑能夠讓瀏覽器盡可能快地繪制網頁:更快的網頁渲染速度可以提高吸引力、增加網頁瀏覽量以及提高轉化率。為了最大程度減少訪客看到空白屏幕的時間,我們需要優化加載的資源及其加載順序。
為幫助說明這一流程,讓我們先從可能的最簡單情況入手,逐步構建我們的網頁,使其包含更多資源、樣式和應用邏輯。在此過程中,我們還會對每一種情況進行優化,以及了解可能出錯的環節。
到目前為止,我們只關注了資源(CSS、JS 或 HTML 文件)可供處理后瀏覽器中會發生的情況,而忽略了從緩存或從網絡獲取資源所需的時間。我們作以下假設:
到服務器的網絡往返(傳播延遲時間)需要 100 毫秒。
HTML 文檔的服務器響應時間為 100 毫秒,所有其他文件的服務器響應時間均為 10 毫秒。
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?1.1.11" 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?1.1.11"></script> </body></html>
添加 JavaScript 和 CSS 之前:
添加 JavaScript 和 CSS 之后:
添加外部 CSS 和 JavaScript 文件將額外增加兩個瀑布請求,瀏覽器差不多會同時發出這兩個請求。不過,請注意,現在 domContentLoaded 事件與 onload 事件之間的時間差小多了。這是怎么回事?
與純 HTML 示例不同,我們還需要獲取并解析 CSS 文件才能構建 CSSOM,要想構建渲染樹,DOM 和 CSSOM 缺一不可。
由于網頁上還有一個阻塞解析器的JavaScript 文件,系統會在下載并解析 CSS 文件之前阻止 domContentLoaded事件:因為 JavaScript 可能會查詢 CSSOM,必須在下載 CSS 文件之后才能執行 JavaScript。
如果我們用內聯腳本替換外部腳本會怎樣?即使直接將腳本內聯到網頁中,瀏覽器仍然無法在構建 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?1.1.11" 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?1.1.11"></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 文件較?。覀冎恍枰淮尉W絡往返便可獲取整個文檔。由于 TCP 傳輸協議工作方式的緣故,較大文件可能需要更多次的往返。因此,在最理想的情況下,上述網頁具有單次往返(最少)關鍵渲染路徑。
現在,我們還以同一網頁為例,但這次使用外部 CSS 文件:
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" 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 頁面的第一個示例包含一項關鍵資源(HTML 文檔);關鍵路徑長度也與 1 次網絡往返相等(假設文件較小),而總關鍵字節數正好是 HTML 文檔本身的傳送大小。
現在,讓我們將其與上面 HTML + CSS 示例的關鍵路徑特性對比一下:
2 項關鍵資源
2 次或更多次往返的最短關鍵路徑長度
9 KB 的關鍵字節
我們同時需要 HTML 和 CSS 來構建渲染樹。所以,HTML 和 CSS 都是關鍵資源:CSS 僅在瀏覽器獲取 HTML 文檔后才會獲取,因此關鍵路徑長度至少為兩次往返。兩項資源相加共計 9KB 的關鍵字節。
現在,讓我們向組合內額外添加一個 JavaScript 文件。
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"> </head> <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script src="app.js?1.1.11"></script> </body></html>
我們添加了 app.js,它既是網頁上的外部 JavaScript 靜態資源,又是一種解析器阻止(即關鍵)資源。更糟糕的是,為了執行 JavaScript 文件,我們還需要進行阻塞并等待 CSSOM;因為JavaScript 可以查詢 CSSOM,因此在下載 style.css 并構建 CSSOM 之前,瀏覽器將會暫停解析。
即便如此,如果我們實際查看一下該網頁的“網絡瀑布”,就會注意到 CSS 和 JavaScript 請求差不多是同時發起的;瀏覽器獲取 HTML,發現兩項資源并發起兩個請求。因此,上述網頁具有以下關鍵路徑特性:
3 項關鍵資源
2 次或更多次往返的最短關鍵路徑長度
11 KB 的關鍵字節
現在,我們擁有了三項關鍵資源,關鍵字節總計達 11 KB,但我們的關鍵路徑長度仍是兩次往返,因為我們可以同時傳送 CSS 和 JavaScript。了解關鍵渲染路徑的特性意味著能夠確定哪些是關鍵資源,此外還能了解瀏覽器如何安排資源的獲取時間。讓我們繼續探討示例。
在與網站開發者交流后,我們意識到我們在網頁上加入的 JavaScript 不必具有阻塞作用:網頁中的一些分析代碼和其他代碼不需要阻止網頁的渲染。了解了這一點,我們就可以向 script 標記添加“async”屬性來解除對解析器的阻止:
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"> </head> <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script src="app.js?1.1.11" async></script> </body></html>
異步腳本具有以下幾個優點:
腳本不再阻止解析器,也不再是關鍵渲染路徑的組成部分。
由于沒有其他關鍵腳本,CSS 也不需要阻止 domContentLoaded 事件。
domContentLoaded 事件觸發得越早,其他應用邏輯開始執行的時間就越早。
因此,我們優化過的網頁現在恢復到了具有兩項關鍵資源(HTML 和 CSS),最短關鍵路徑長度為兩次往返,總關鍵字節數為 9 KB。
最后,如果 CSS 樣式表只需用于打印,那會如何呢?
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" 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?1.1.11" 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事件無關。
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com