利用 JavaScript 增加互動

Ilya Grigorik
Ilya Grigorik

發布日期: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 變更文字的文字,甚至覆寫計算出的顯示樣式屬性,從「無」覆寫為「內嵌」。頁面現在會顯示「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,我們執行指令碼時該怎麼辦?答案對效能的影響不大,像是瀏覽器延遲下載和建構 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 屬性新增至 <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 建構,進而大幅提升效能。

意見回饋