標籤:重繪 pack 標籤 handle 就會 classname change lse lock
javasciprt效能最佳化
本文主要是在我讀《高效能Javascript》之後,想要記錄下一些有用的最佳化方案,並且就我本身的一些經驗,來大家一起分享下,
Javascript的載入與執行
大家都知道,瀏覽器在解析DOM樹的時候,當解析到script標籤的時候,會阻塞其他的所有任務,直到該js檔案下載、解析執行完成後,才會繼續往下執行。因此,這個時候瀏覽器就會被阻塞在這裡,如果將script標籤放在head裡的話,那麼在該js檔案載入執行前,使用者只能看到空白的頁面,這樣的使用者體驗肯定是特別爛。對此,常用的方法有以下:
將所有的script標籤都放到body最底部,這樣可以保證js檔案是最後載入並執行的,可以先將頁面展現給使用者。但是,你首先得清楚,頁面的首屏渲染是否依賴於你的部分js檔案,如果是的話,則需要將這一部分js檔案放到head上。
使用defer,比如下面這種寫法。使用defer這種寫法時,雖然瀏覽器解析到該標籤的時候,也會下載對應的js檔案,不過它並不會馬上執行,而是會等到DOM解析完後(DomContentLoader之前)才會執行這些js檔案。因此,就不會阻塞到瀏覽器。
動態載入js檔案,通過這種方式,可以在頁面載入完成後,再去載入所需要的代碼,也可以通過這種方式實現js檔案懶載入/按需載入,比如現在比較常見的,就是webpack結合vue-router/react-router實現按需載入,只有訪問到具體路由的時候,才載入相應的代碼。具體的方法如下:
1.動態插入script標籤來載入指令碼,比如通過以下代碼
function loadScript(url, callback) {
const script = document.createElement(‘script‘);
script.type = ‘text/javascript‘;
// 處理IE
if (script.readyState) {
script.onreadystatechange = function () {
if (script.readyState === ‘loaded‘ || script.readyState === ‘complete‘) {
script.onreadystatechange = null;
callback();
}
}
} else {
// 處理其他瀏覽器的情況
script.onload = function () {
callback();
}
}
script.src = url;
document.body.append(script);
}
// 動態載入js
loadScript(‘file.js‘, function () {
console.log(‘載入完成‘);
})
2.通過xhr方式載入js檔案,不過通過這種方式的話,就可能會面臨著跨域的問題。例子如下:
const xhr = new XMLHttpRequest();
xhr.open(‘get‘, ‘file.js‘);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304>) {
const script = document.createElement(‘script‘);
script.type = ‘text/javascript‘;
script.text = xhr.responseText;
document.body.append(script);
}
}
}
3.將多個js檔案合并為同一個,並且進行壓縮。 原因:目前瀏覽器大多已經支援並行下載js檔案了,但是並發下載還是有一定的數量限制了(基於瀏覽器,一部分瀏覽器只能下載4個),並且,每一個js檔案都需要建立一次額外的http串連,載入4個25KB的檔案比起載入一個100KB的檔案消耗的時間要大。因此,我們最好就是將多個js檔案合并為同一個,並且進行代碼壓縮。
javascript範圍
當一個函數執行的時候,會產生一個執行內容,這個執行內容定義了函數執行時的環境。當函數執行完畢後,這個執行內容就會被銷毀。因此,多次調用同一個函數會導致建立多個執行內容。每隔執行內容都有自己的範圍鏈。相信大家應該早就知道了範圍這個東西,對於一個函數而言,其第一個範圍就是它函數內部的變數。在函數執行過程中,每遇到一個變數,都會搜尋函數的範圍鏈找到第一個匹配的變數,首先尋找函數內部的變數,之後再沿著範圍鏈逐層尋找。因此,若我們要訪問最外層的變數(全域變數),則相比直接存取內部的變數而言,會帶來比較大的效能損耗。因此,我們可以將經常使用的全域變數引用儲存在一個局部變數裡。
const a = 5;
function outter () {
const a = 2;
function inner () {
const b = 2;
console.log(b); // 2
console.log(a); // 2
}
inner();
}
對象的讀取
javascript中,主要分為字面量、局部變數、數組元素和對象這四種。訪問字面量和局部變數的速度最快,而訪問數組元素和對象成員相對較慢。而訪問對象成員的時候,就和範圍鏈一樣,是在原型鏈(prototype)上進行尋找。因此,若尋找的成員在原型鏈位置太深,則訪問速度越慢。因此,我們應該儘可能的減少對象成員的尋找次數和嵌套深度。比如以下代碼
// 進行兩次對象成員尋找
function hasEitherClass(element, className1, className2) {
return element.className === className1 || element.className === className2;
}
// 最佳化,如果該變數不會改變,則可以使用局部變數儲存尋找的內容
function hasEitherClass(element, className1, className2) {
const currentClassName = element.className;
return currentClassName === className1 || currentClassName === className2;
}
DOM操作最佳化
最小化DOM的操作次數,儘可能的用javascript來處理,並且儘可能的使用局部變數儲存DOM節點。比如以下的代碼:
// 最佳化前,在每次迴圈的時候,都要擷取id為t的節點,並且設定它的innerHTML
function innerHTMLLoop () {
for (let count = 0; count < 15000; count++) {
document.getElementById(‘t‘).innerHTML += ‘a‘;
}
}
// 最佳化後,
function innerHTMLLoop () {
const tNode = document.getElemenById(‘t‘);
const insertHtml = ‘‘;
for (let count = 0; count < 15000; count++) {
insertHtml += ‘a‘;
}
tNode.innerHtml += insertHtml;
}
儘可能的減少重排和重繪,重排和重匯可能會代價非常昂貴,因此,為了減少重排重匯的發生次數,我們可以做以下的最佳化
1.當我們要對Dom的樣式進行修改的時候,我們應該儘可能的合并所有的修改並且一次處理,減少重排和重匯的次數。
// 最佳化前
const el = document.getElementById(‘test‘);
el.style.borderLeft = ‘1px‘;
el.style.borderRight = ‘2px‘;
el.style.padding = ‘5px‘;
// 最佳化後,一次性修改樣式,這樣可以將三次重排減少到一次重排
const el = document.getElementById(‘test‘);
el.style.cssText += ‘; border-left: 1px ;border-right: 2px; padding: 5px;‘
2.當我們要批量修改DOM節點的時候,我們可以將DOM節點隱藏掉,然後進行一系列的修改操作,之後再將其設定為可見,這樣就可以最多隻進行兩次重排。具體的方法如下:
// 未最佳化前
const ele = document.getElementById(‘test‘);
// 一系列dom修改操作
// 最佳化方案一,將要修改的節點設定為不顯示,之後對它進行修改,修改完成後再顯示該節點,從而只需要兩次重排
const ele = document.getElementById(‘test‘);
ele.style.display = ‘none‘;
// 一系列dom修改操作
ele.style.display = ‘block‘;
// 最佳化方案二,首先建立一個文檔片段(documentFragment),然後對該片段進行修改,之後將文檔片段插入到文檔中,只有最後將文檔片段插入文檔的時候會引起重排,因此只會觸發一次重排。。
const fragment = document.createDocumentFragment();
const ele = document.getElementById(‘test‘);
// 一系列dom修改操作
ele.appendChild(fragment);
3.使用事件委託:事件委託就是將目標節點的事件移到父節點來處理,由於瀏覽器冒泡的特點,當目標節點觸發了該事件的時候,父節點也會觸發該事件。因此,由父節點來負責監聽和處理該事件。那麼,它的優點在哪裡呢?假設你有一個列表,裡面每一個清單項目都需要綁定相同的事件,而這個列表可能會頻繁的插入和刪除。如果按照平常的方法,你只能給每一個清單項目都綁定一個事件處理器,並且,每當插入新的清單項目的時候,你也需要為新的清單項目註冊新的事件處理器。這樣的話,如果清單項目很大的話,就會導致有特別多的事件處理器,造成極大的效能問題。而通過事件委託,我們只需要在清單項目的父節點監聽這個事件,由它來統一處理就可以了。這樣,對於新增的清單項目也不需要做額外的處理。而且事件委託的用法其實也很簡單:
function handleClick(target) {
// 點擊清單項目的處理事件
}
function delegate (e) {
// 判斷目標對象是否為清單項目
if (e.target.nodeName === ‘LI‘) {
handleClick(e.target);
}
}
const parent = document.getElementById(‘parent‘);
parent.addEventListener(‘click‘, delegate);
高效能Javascript