作者 | Mahdhi Rezvi
譯者 | 王強
策劃 | 李俊辰
轉發鏈接:https://mp.weixin.qq.com/s/veJ6mhxd5XwVG4OF7i0VYQ
前言
這篇文章列舉了一些技巧,可幫助你寫出更好的 JAVAScript 代碼,從而提高性能。
本文最初發布于 Medium 網站,經原作者授權由 InfoQ 中文站翻譯并分享。
JavaScript 已經成為有史以來最受歡迎的編程語言之一。從 W3Tech 的數據來看,全世界將近 96%的網站都在使用它。關于 Web 有一個關鍵的事實是,你無法控制訪問網站的用戶所用設備。當用戶訪問你的網站時,使用的可能是高端設備也可能是低端設備,網絡連接條件也有好有差。這意味著你必須盡可能優化自己的網站,以滿足任何用戶的需求。
附帶提一下,請共享和重用你的 JS 組件,以在高質量代碼(寫起來需要花費時間)和合理的交付時間之間保持適當的平衡。你可以使用 Bit 等流行工具將任何項目中的組件(普通 JS、TS、React、Vue 等)共享到 Bit 的組件中心,用不了多大功夫。
1、刪除未使用的代碼和功能
你的應用程序包含的代碼越多,就需要將更多的數據傳輸到客戶端。瀏覽器也需要更多時間來分析和解釋代碼。
有時,你可能打包了很多根本用不到的功能。最好只在開發環境中保留這些額外的代碼,而不要將其推送到生產環境中,以免給客戶端的瀏覽器增加負擔。
要不斷問自己,某個功能或代碼段是否是必要的。
你可以手動移除未使用的代碼,也可以使用 Uglify 或谷歌的 Closure Compiler 之類的工具刪除它們。你還可以使用一種被稱為搖樹優化的技術從應用程序中刪除未使用的代碼。Webpack 這類打包軟件提供了這種技術,詳情可以參考這里:
https://www.infoq.cn/article/dcKcJiT8aeEBNZbdotFF
如果要刪除未使用的 npm 軟件包,可以使用命令 npm prune,詳細信息參考 NPM 文檔。
https://docs.npmjs.com/cli-commands/prune.html
2、盡可能的緩存
緩存可以減少延遲和網絡流量,從而減少了顯示資源表示所需的時間,以提高網站的速度和性能。緩存可以借助 Cache API 或 HTTP caching 來實現。你可能想知道內容更改時會發生什么。當滿足某些條件(例如發布新內容)時,上述緩存機制能夠處理和重新生成緩存。
3、避免內存泄漏
作為一種高級語言,JS 會負責一些底層管理工作,例如內存管理。垃圾回收是大多數編程語言共有的過程。用外行術語來說,垃圾收集就是收集并釋放已分配給對象,但目前尚未在程序的任何部分中使用的內存。在 C 這樣的編程語言中,開發人員必須使用 malloc() 和 dealloc() 函數來處理內存分配和釋放操作。
雖然在 JavaScript 中垃圾回收是自動執行的,但在某些情況下它也不是完美的。在 JavaScript ES6 中,引入了 Map 和 Set 及其“weaker”的同級對象。被稱為 WeakMap 和 WeakSet 的“較弱”對應項持有對對象的“弱”引用。它們使未引用的值能夠被垃圾回收,從而防止內存泄漏。你可以在此處了解有關 WeakMaps 的更多信息:
https://blog.bitsrc.io/understanding-weakmaps-in-javascript-6e323d9eec81
4、盡早打破循環
超大循環肯定會耗費很多的時間,所以你應該盡早打破這些超大循環。你可以用 break 關鍵字和 continue 關鍵字來做這件事,從而編寫更高效的代碼。
在下面的示例中,如果你沒有從循環中 break,則你的代碼將循環運行 1000000000 (10億)次,顯然會過載的。
let arr = new Array(1000000000).fill('----');
arr[970] = 'found';
for (let i = 0; i < arr.length; i++) {
if (arr[i] === 'found') {
console.log("Found");
break;
}
}
在下面的示例中,如果你在循環不符合你的條件時沒有 continue,則你仍將運行該函數 1000000000 次。我們僅在數組元素處于偶數位置時處理它。這將循環執行減少了近一半。
let arr = new Array(1000000000).fill('----');
arr[970] = 'found';
for (let i = 0; i < arr.length; i++) {
if(i%2!=0){
continue;
};
process(arr[i]);
}
你可以在此處詳細了解循環和性能的關系:
https://www.oreilly.com/library/view/high-performance-javascript/9781449382308/ch04.html
5、最小化變量計算的次數
為了減少計算變量的次數,可以使用閉包。通俗來說,JavaScript 中的閉包使你可以從內部函數訪問外部函數作用域。每次創建函數(不調用)時都會創建閉包。內部函數將有權訪問外部作用域的變量,即使在返回外部函數之后也是如此。
我們來看兩個例子。這些示例均來自 Bret 的博客。
function findCustomerCity(name) {
const texasCustomers = ['John', 'Ludwig', 'Kate'];
const californiaCustomers = ['Wade', 'Lucie','Kylie'];
return texasCustomers.includes(name) ? 'Texas' :
californiaCustomers.includes(name) ? 'California' : 'Unknown';
};
如果你多次調用上面的函數,那么每次都會創建一個新對象。每次調用時,變量 texasCustomers 和 californiaCustomers 都會導致不必要的內存重分配。
function findCustomerCity() {
const texasCustomers = ['John', 'Ludwig', 'Kate'];
const californiaCustomers = ['Wade', 'Lucie','Kylie'];
return name => texasCustomers.includes(name) ? 'Texas' :
californiaCustomers.includes(name) ? 'California' : 'Unknown';
};
let cityOfCustomer = findCustomerCity();
cityOfCustomer('John');//Texas
cityOfCustomer('Wade');//California
cityOfCustomer('Max');//Unknown
在上面的示例中,借助于閉包,返回到變量 cityOfCustomer 的內部函數可以訪問外部函數 findCustomerCity() 的常量。而且,每當以傳遞的名稱作為參數調用內部函數時,都無需再次實例化常量。要了解關于閉包的更多信息,建議你閱讀 Prashant 的博客文章:
https://medium.com/@prashantramnyc/javascript-closures-simplified-d0d23fa06ba4
6、盡量減少 DOM 訪問
與其他 JavaScript 語句相比,訪問 DOM 的速度很慢。如果你對 DOM 進行更改,觸發了布局的重新繪制,那么就得等好一陣子了。
為了減少訪問 DOM 元素的次數,請先訪問一次,然后將其用作局部變量。完成需求后,請一定將其設置為 null 來移除該變量的值。這將防止內存泄漏,因為這會觸發垃圾回收過程。
7、壓縮文件
通過壓縮方法(例如 Gzip)可以減小 JavaScript 文件的大小。較小的文件會提升你的網站性能,因為瀏覽器只需下載較小的資產即可。
這類壓縮手段最多可以減少 80%的文件大小。在此處閱讀有關壓縮的更多信息:
https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/optimize-encoding-and-transfer#text_compression_with_gzip
8、縮小最終代碼
有人認為縮小和壓縮是相同的,其實不然。在壓縮中,我們使用特殊算法來改變文件的輸出大小;在縮小時,我們需要刪除 JavaScript 文件中的注釋和多余的空格。可以在網上找到許多工具和軟件包來幫助完成這一過程。縮小已成為頁面優化的標準做法,也是前端優化的主要步驟之一。
縮小可以讓文件大小最多減少 60%。你可以在此處閱讀有關縮小的更多信息:
https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/optimize-encoding-and-transfer#minification_preprocessing_context-specific_optimizations
9、使用 Throttle 和 Debounce
我們可以使用 Throttle(節流)和 Debounce(防抖)這兩種技術來嚴格控制代碼需要處理事件的次數。
節流是指定函數可以超時的最大次數。例如,“每 1000 毫秒最多執行一次 onkeyup 事件函數”。也就是說哪怕你每秒敲 20 個鍵,該事件每秒也只會觸發一次。這將減少代碼的負擔。
另一方面,防抖是指定自上次執行相同函數以來再次運行該函數的最短持續時間。換句話說,“上次調用函數后果最少 600 毫秒才執行此函數”。要了解有關節流和防抖的更多信息,這里有一篇快速入門:
https://css-tricks.com/the-difference-between-throttling-and-debouncing/
你可以實現自己的防抖和節流函數,也可以從 Lodash 和 Underscore 之類的庫中導入它們。
10、避免使用 Delete 關鍵字
delete 關鍵字用于從對象中刪除屬性。這個關鍵字的性能表現不怎么好,預計它將在未來的更新中修復。
或者,你可以簡單地將不需要的屬性設置為 undefined。
const object = {name:"Jane Doe", age:43};
object.age = undefined;
你還可以使用 Map 對象,Bret 認為它的 delete 方法會更快。
11、使用異步代碼防止線程阻塞
你應該知道 JavaScript 默認情況下是同步的和單線程的。但是在某些情況下,你的代碼需要很大的計算量。代碼本質上是同步的,意味著一段代碼運行時將阻止其他代碼語句運行,直到前者完成執行為止。這會降低整體性能。
但是我們可以通過異步代碼來避免這種情況。異步代碼以前以回調的形式編寫,但是 ES6 引入了一種處理異步代碼的新樣式。這種新樣式被稱為 Promise。你可以在 MDN 的官方文檔中了解有關回調和 Promise 的更多信息。
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing
可是等等……
JavaScript 默認情況下是同步的,并且也是單線程的。
如何在單個線程上運行異步代碼呢?這是很多人感到困惑的地方。做到這一點,主要依賴運行在瀏覽器后臺的 JavaScript 引擎。JavaScript 引擎是執行 JavaScript 代碼的計算機程序或解釋器。JavaScript 引擎可以用多種語言編寫。例如,支持 Chrome 瀏覽器的 V8 引擎使用 C++ 編寫的,而支持 Firefox 瀏覽器的 SpiderMonkey 引擎使用 C 和 C++ 編寫的。
這些 JavaScript 引擎可以在后臺處理任務。根據 Brian 的說法,調用棧可以識別 Web API 的函數,并將其交給瀏覽器處理。瀏覽器完成這些任務后,它們將返回并作為回調被推上堆棧。
你可能想知道 Node.js 是怎么做這些工作的,畢竟它沒有瀏覽器的幫助。實際上,支持 Chrome 的那個 V8 引擎也是 Node.js 背后的支撐。這里有 Salil 的一篇很棒的博客文章,解釋了 Node 生態系統中的這一過程。
https://medium.com/better-programming/is-node-js-really-single-threaded-7ea59bcc8d64
12、使用代碼拆分
如果你有使用 Google Light House 的經驗,肯定會熟悉一種稱為“first contentful paint”的指標。它是 Lighthouse 報告的 Performance 部分中跟蹤的六個指標之一。
First Contentful Paint(FCP)衡量用戶轉到你的頁面后瀏覽器渲染第一段 DOM 內容所花費的時間。頁面上的圖像、非白色<canvas>元素和 SVG 被視為 DOM 內容;iframe 內部不包含任何內容。
獲得更高的 FCP 分數的最佳方法之一是使用代碼拆分。代碼拆分是一種在傳輸開始時僅將必要的模塊發送給用戶的技術。通過減小最初發送的載荷大小,這將極大地影響 FCP 分數。
流行的模塊打包器(例如 webpack)可為你提供代碼拆分功能。你還可以利用原生 ES 模塊來單獨加載各個模塊。你可以在此處詳細了解有關原生 ES 模塊的信息。
https://blog.bitsrc.io/understanding-es-modules-in-javascript-a28fec420f73
13、使用 async 和 defer
在現代網站中,腳本比 HTML 更為密集,其大小更大且消耗更多的處理時間。默認情況下,瀏覽器必須等待腳本下載和執行完畢后,再處理頁面的其余部分。
于是笨重的腳本可能會阻止網頁的加載。為了避免這種情況,JavaScript 為我們提供了兩種分別稱為 async 和 defer 的技術。你只需將這些屬性添加到<script>標記中即可。
Async 會讓瀏覽器在不影響渲染的情況下加載腳本。換句話說,頁面不會等待 async 腳本,而是先處理和顯示內容。
Defer 是讓瀏覽器在渲染完成后加載腳本。如果同時指定它們兩者,則 async 在現代瀏覽器上更優先,而支持 defer 但不支持 async 的老式瀏覽器將回退為 defer。
這兩個屬性可以幫助你大幅減少頁面加載時間。我強烈建議你閱讀 Flavio 的這篇博客文章。
https://flaviocopes.com/javascript-async-defer/
14、在后臺運行 CPU 密集型任務
可以使用 Web Worker 在后臺線程中運行腳本。如果你有一些高強度的任務,可以將它們分配給 Web Worker,這些 WebWorker 可以在不干擾用戶界面的情況下運行它們。創建后,Web Worker 可以將消息發布到該代碼指定的事件處理程序來與 JavaScript 代碼通信,反之亦然。
作者 | Mahdhi Rezvi
譯者 | 王強
策劃 | 李俊辰
轉發鏈接:https://mp.weixin.qq.com/s/veJ6mhxd5XwVG4OF7i0VYQ