JavaScript >> Javascript 文檔 >  >> JavaScript

加速你的 JavaScript,第 4 部分

在過去的幾周里,我一直在探索各種加速 JavaScript 的技術。第 1 部分介紹瞭如何處理執行過多的循環。第 2 部分側重於功能過多的功能,並教授了排隊和記憶等技術以減輕工作量。第 3 部分將討論擴展到通過記憶和切換到迭代算法來處理遞歸。第 4 部分,本系列的最後一部分,重點介紹了過多的 DOM 交互。

我們都知道 DOM 很慢……真的很慢……而且它是最常見的性能問題來源之一。緩慢的原因在於 DOM 更改會改變頁面的用戶界面,而重繪頁面是一項昂貴的操作。太多的 DOM 更改意味著大量的重繪,因為每個更改都必須按順序和同步地應用,以確保正確的最終結果。這個過程稱為回流,是瀏覽器最昂貴的功能之一。回流發生在不同的時間點:

  • 當您添加或移除 DOM 節點時。
  • 當您動態應用樣式時(例如 element.style.width="10px" )。
  • 當您檢索必須計算的度量時,例如訪問 offsetWidth , clientHeight ,或任何計算的 CSS 值(通過 getComputedStyle() 在兼容 DOM 的瀏覽器或 currentStyle 中 在 IE 中),而 DOM 更改排隊等待進行。

因此,它們的關鍵是通過 DOM 交互限制頁面上發生的重排次數。大多數瀏覽器在 JavaScript 執行時不會更新 DOM。相反,它們將 DOM 交互排隊,並在腳本完成執行後按順序應用它們。與 JavaScript 執行一樣,用戶無法在重排時與瀏覽器交互。 (當顯示長時間運行的腳本對話框時會發生重排,因為它代表 JavaScript 執行的中斷,允許 UI 更新。)

有兩種基於 DOM 更改來緩解回流的基本方法。首先是在活動 DOM 結構(表示可見元素的部分)之外執行盡可能多的更改。經典的例子是在一個文檔中添加一些 DOM 節點:

for (var i=0; i < items.length; i++){
    var item = document.createElement("li");
    item.appendChild(document.createTextNode("Option " + i);
    list.appendChild(item);
}

這段代碼效率低下,因為它每次通過循環都會觸及活動的 DOM。為了提高性能,您應該最小化這個數字。在這種情況下,最好的選擇是創建一個文檔片段作為創建的 li 的中間佔位符 元素,然後使用它將所有元素添加到其父元素:

var fragment = document.createDocumentFragment();
for (var i=0; i < items.length; i++){
    var item = document.createElement("li");
    item.appendChild(document.createTextNode("Option " + i);
    fragment.appendChild(item);
}
list.appendChild(fragment);

此版本的代碼僅在最後一行觸及實時 DOM 一次。在此之前,文檔片段用於保存中間結果。由於文檔片段沒有可視化表示,因此在修改時不會導致重排。文檔片段也不能添加到實時 DOM 中,因此將其傳遞到 appendChild() 實際上將所有片段的孩子添加到 list 而不是片段本身。

避免不必要的重排的第二種方法是在對它進行操作之前從活動 DOM 中刪除一個節點。您可以通過兩種方式從活動 DOM 中刪除節點:1)通過 removeChild() 從 DOM 中刪除節點 或 replaceChild() , 或 2) 設置 display 樣式為 "none" .一旦 DOM 修改完成,則必須反轉該過程並且必須將節點重新添加到活動 DOM 中。上一個示例的另一種方法可能是:

list.style.display = "none";
for (var i=0; i < items.length; i++){
    var item = document.createElement("li");
    item.appendChild(document.createTextNode("Option " + i);
    list.appendChild(item);
}
list.style.display = "";

將列表的顯示設置為“無”會將其從實時 DOM 中刪除,因為它不再具有可視化表示。在將顯示設置回默認值之前,可以安全地添加所有項目。

多次重排的另一個常見來源是通過 style 更改元素的外觀 財產。例如:

element.style.backgroundColor = "blue";
element.style.color = "red";
element.style.fontSize = "12em";

此代碼具有三個樣式更改……以及三個重排。每次更改此元素的樣式都會發生重排。如果您要對元素的樣式進行大量更改,最好將它們分組到 CSS 類中,然後使用 JavaScript 更改類,而不是手動應用單獨的樣式更改。例如:

.newStyle {
    background-color: blue;
    color: red;
    font-size: 12em;
}

然後 JavaScript 變成了一行:

element.className = "newStyle";

更改元素計數的類允許在一次重排中一次應用所有樣式。從長遠來看,這更有效,也更易於維護。

由於 DOM 在幾乎所有事情上都非常慢,因此緩存從 DOM 中檢索到的結果非常重要。這對於導致回流的屬性訪問很重要,例如 offsetWidth ,但總的來說也很重要。例如,以下是非常低效的:

document.getElementById("myDiv").style.left = document.getElementById("myDiv").offsetLeft +
    document.getElementById("myDiv").offsetWidth + "px";

getElementById() 的三個調用 這是問題所在。訪問 DOM 是昂貴的,這是三個 DOM 調用來訪問完全相同的元素。代碼最好這樣寫:

var myDiv = document.getElementById("myDiv");
myDiv.style.left = myDiv.offsetLeft + myDiv.offsetWidth + "px";

現在,通過刪除冗餘調用,已將總 DOM 操作的數量降至最低。始終緩存多次使用的 DOM 值以避免性能損失。

HTMLCollection 可能是最令人震驚的緩慢屬性訪問的罪犯 類型。這是必須表示節點集合時從 DOM 返回的對像類型,childNodes 的類型也是如此 屬性並且是從 getElementsByTagName() 返回的類型 .一個 HTMLCollection 在許多方面可能像一個數組,但它實際上是一個活生生的實體,隨著 DOM 結構的變化而變化。每次訪問 HTMLCollection 上的屬性時 對象,它實際上再次在 DOM 中查詢與原始條件匹配的所有節點。這意味著下面是一個無限循環:

var divs = document.getElementsByTagName("div");
for (var i=0; i < divs.length; i++){  //infinite loop
    document.body.appendChild(document.createElement("div"));
}

這段代碼是一個無限循環,因為每次都有一個新的 div 元素被添加到文檔中,divs 集合會使用該新信息進行更新。這意味著 i 永遠不會達到 divs.length 因為 divs.length 每次循環增加一。每次divs.length 被訪問時,它的集合被更新,這使得它比訪問常規數組的 length 更昂貴 財產。處理 HTMLCollection 時 對象,最好盡量減少訪問其屬性的次數。您可以通過簡單地緩存 length 來極大地加速循環 在局部變量中:

var divs = document.getElementsByTagName("div");
for (var i=0, len=divs.length; i < len; i++){  //not an infinite loop
    document.body.appendChild(document.createElement("div"));
}

此代碼不再代表無限循環,因為 len 的值 在每次迭代中保持不變。緩存值也更有效,因此不會多次查詢文檔。

“加快 JavaScript 速度”系列到此結束。我希望你已經學到了足夠的知識來避免長時間運行的腳本對話框並讓你的代碼更快。我所討論的許多主題並不新鮮。我只是將它們全部展示在一個地方,以便其他人可以輕鬆找到這些信息。如果您有其他想看我介紹的主題,請隨時在評論中留言或直接與我聯繫。

翻譯

  • 中文(簡體)

Tutorial JavaScript 教程
  1. HTTP 狀態碼列表

  2. 使用 fetch 在 JavaScript 中上傳文件

  3. 無法再通過 CMD+D 打開開髮菜單或通過 CMD+R 重新加載

  4. Async-Ray 和所有 2 個 Promise 實用程序庫都是用 TypeScript 編寫的

  5. 在 JavaScript 中返回其他函數的函數的力量

  6. 理解 React Hooks 的快速指南

  7. 你不知道的傑克:學會讓你的代碼更安全

  1. 構建真實世界的無服務器應用程序

  2. Vuex ORM 快速介紹

  3. 如何在 JavaScript 中處理“未定義”

  4. JavaScript 中的一切都不是對象

  5. 更多可用的表單——控制滾動位置

  6. 如何使 SolidJS 應用程序從子文件夾中工作

  7. 提交新交易時,moment.js 中的日期不會更改,仍然保存上次日期,直到刷新服務器

  1. D3.js 入門

  2. 顯示統計

  3. Kinsta Kingpin:約書亞·布羅頓訪談

  4. 從 0 到 5.000.000 每月瀏覽量