利用 JavaScript 增加互動

Ilya Grigorik
Ilya Grigorik

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 相容學生!」

  • 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 元素的顯示屬性從無變更為內嵌。結果呢?我們已經設定競爭狀況。

如果我們要執行指令碼,瀏覽器尚未完成下載及建構 CSSOM,該怎麼辦?答案很簡單,效能不佳:在完成下載及建構 CSSOM 之前,瀏覽器會延遲指令碼執行和 DOM 建構作業。

簡單來說,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

<!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>

試用

在指令碼標記中加入非同步關鍵字後,瀏覽器在等待指令碼可供使用時,就不要封鎖 DOM 建構程序,成效將大幅提升。

意見回饋: