發布日期:2013 年 12 月 31 日
我們可以使用 JavaScript 修改網頁的各個層面,包括內容、樣式,以及對使用者互動的回應。不過,JavaScript 也可能會阻斷 DOM 建構作業,並延遲網頁轉譯作業。為提供最佳效能,請讓 JavaScript 為非同步,並從關鍵算繪路徑中移除任何不必要的 JavaScript。
摘要
- JavaScript 可查詢及修改 DOM 和 CSSOM。
- CSSOM 上的 JavaScript 執行區塊。
- 除非明確宣告為非同步,否則 JavaScript 會封鎖 DOM 建構作業。
JavaScript 是一種在瀏覽器中執行的動態語言,可讓我們變更網頁行為的各個層面:我們可以透過在 DOM 樹狀結構中新增及移除元素來修改內容;修改各個元素的 CSSOM 屬性;處理使用者輸入內容等等。為說明這一點,請參考以下情況:當先前的「Hello World」範例變更為內嵌簡短指令碼時,會發生什麼事:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script</title>
</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 中。接著,當我們取得參照後,就可以透過 .textContent 變更文字,甚至將計算出的顯示樣式屬性從「none」覆寫為「inline」。頁面現在會顯示「Hello interactive students!」。
JavaScript 也允許我們在 DOM 中建立、設定樣式、附加及移除新元素。從技術層面來說,整個網頁可能只是一個大型 JavaScript 檔案,用來逐一建立及設定元素樣式。雖然這麼做可以達到目的,但在實際操作中,使用 HTML 和 CSS 會更簡單。在 JavaScript 函式的第二部分,我們會建立新的 div 元素、設定文字內容、設定樣式,然後將其附加至主體。
這樣一來,我們就修改了現有 DOM 節點的內容和 CSS 樣式,並在文件中新增一個全新的節點。我們的網頁不會獲得任何設計獎項,但這正是 JavaScript 提供的強大功能和彈性。
不過,雖然 JavaScript 提供許多強大的功能,但也會在網頁的轉譯方式和時間上造成許多額外的限制。
首先,請注意,在先前的範例中,內嵌指令碼位於頁面底部附近。這是因為您可以自行嘗試,但如果將指令碼移至 <span>
元素上方,您會發現指令碼失敗,並顯示無法在文件中找到任何 <span>
元素參照的錯誤訊息,也就是 getElementsByTagName('span')
會傳回 null
。這項特性很重要:指令碼會在文件中插入的位置執行。HTML 剖析器遇到指令碼標記時,會暫停建構 DOM 的程序,並將控制權交給 JavaScript 引擎;JavaScript 引擎執行完畢後,瀏覽器會接續上次的進度,繼續建構 DOM。
換句話說,我們的指令碼區塊無法在網頁中找到任何元素,因為這些元素尚未處理!換句話說,執行內嵌指令碼會阻斷 DOM 建構,也會延遲初始轉譯作業。
在頁面中加入指令碼的另一個微妙屬性是,這些指令碼不僅可以讀取及修改 DOM,還能讀取及修改 CSSOM 屬性。事實上,在本例中,我們將 span 元素的顯示屬性從「none」變更為「inline」,就是為了達到這個效果。最終結果是我們現在有競爭狀況。
如果瀏覽器尚未完成下載及建構 CSSOM,我們要如何執行指令碼?答案對效能來說不太理想:瀏覽器會延遲執行指令碼和建構 DOM,直到完成下載及建構 CSSOM 為止。
簡而言之,JavaScript 會在 DOM、CSSOM 和 JavaScript 執行作業之間引入許多新的依附元件。這可能會導致瀏覽器在處理及轉譯畫面上的網頁時,發生顯著的延遲:
- 指令碼在文件中的確切位置很重要。
- 瀏覽器遇到指令碼標記時,DOM 建構作業會暫停,直到指令碼執行完畢為止。
- JavaScript 可查詢及修改 DOM 和 CSSOM。
- JavaScript 執行作業會暫停,直到 CSSOM 就緒為止。
在很大程度上,「最佳化關鍵轉譯路徑」是指瞭解並最佳化 HTML、CSS 和 JavaScript 之間的依附元件圖表。
剖析器封鎖與非同步 JavaScript
根據預設,JavaScript 執行作業會採取「剖析器阻斷」方式:當瀏覽器在文件中遇到指令碼時,必須暫停 DOM 建構作業,將控制權交給 JavaScript 執行階段,並在繼續執行 DOM 建構作業前讓指令碼執行。在先前的範例中,我們已透過內嵌指令碼實作這項功能。事實上,除非您編寫額外的程式碼來延遲執行,否則內嵌指令碼一律會遭到剖析器阻擋。
那麼,如果使用指令碼標記加入的指令碼呢?以先前的範例為例,將程式碼解壓縮到個別檔案中:
<!DOCTYPE html>
<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,並在指令碼準備就緒時執行,例如在從快取或遠端伺服器擷取檔案後。
為達成這項目標,我們將 async
屬性新增至 <script>
元素:
<!DOCTYPE html>
<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>
在指令碼標記中加入 async 關鍵字,可讓瀏覽器在等待指令碼可用時,不阻擋 DOM 建構,進而大幅提升效能。