嗨~ 歡迎閱讀最新一期的全端雙週報,一轉眼本期已經到了第 40 期的雙週報!在這期我們會談記憶體洩漏 (memory leak) 的主題,與此同時,一樣會整理推薦一讀的連結與讀者們分享。
在開始這期的雙週報主題文前,想提醒有訂閱 E+ 的讀者,在 2024 年底也預計加開年末直播特別場。我們將邀請獲史丹佛大學 Life Design Lab 認證的教練 Alex,帶著大家做年末反思與新年目標設定 。
只要在 11/23 前,完成 E+ 社群的小任務,就能獲得年末直播的入場券。對直播工作坊感興趣的讀者,歡迎加入 E+ 一起參與 (E+ 連結見此)。
以上,接著讓我們來談本週的主題內容吧。
[前端] 如何避免前端的記憶體洩漏?
在開發軟體時,不論是什麼系統,都會有記憶體體有限的狀況,而前端應用的開發也不例外。從前端的角度來看,我們在程式運行時,每新增一個變數、每呼叫一個函式,都會需要佔用記憶體的空間。
然而,記憶體是有限的。換句話說,當今天正在跑的程式需要更多記憶體,但是執行環境的記憶體不夠,就會出問題。而這種問題又被稱為記憶體洩漏。身為開發者,記憶體洩漏是我們要盡可能去避免的。
前端常見的記憶體洩漏原因
要避免發生記憶體洩漏的慘劇,身為前端工程師,我們要確保寫的程式不會出現記憶體洩漏的問題。因此以下我們將討論幾個常見的記憶體洩漏原因
全域變數
function getExampleOne(arg) {
exampleOne = "這樣做會出什麼問題?";
}
function getExampleTwo() {
this.exampleTwo = "這樣呢? 會有問題嗎?";
}
getExampleOne()
getExampleTwo()
上面的程式碼中,大家有看出問題所在嗎? 可以看到 exampleOne
跟 exampleTwo
都會是全域變數,因為前者是沒有用 var
、let
或 const
關鍵字賦值,所以會變全域變數,而後者則是因為在全域的函式的 this
是指向全域物件。這將導致如果你實際上沒有一直需要用這兩個變數,他們仍會掛在全域物件。
這個跟單例模式 (singleton pattern) 使用時有類似的問題。選擇這個設計模式本身有其好處,但同時也要注意,如果長時間存在沒有用到的全域單例,不釋放掉就會導致記憶體的浪費。
事件監聽沒有移除
在寫前端應用時,我們經常會需要監聽事件,而這也是常見的記憶體外洩之處。舉例來說:
const longString = new Array(100000).join('x');
document.addEventListener('keyup', function() {
doSomething(longString);
});
上面這段程式碼,在 keyup
事件發生時,執行某個匿名函式。因為是匿名函式,所以沒有清除的狀況下,longString
就會一直在記憶體中。如果後續沒有用 longString
就意味著我們佔著記憶體沒做事。因此務必確保,在做事件監聽時,要適時用 removeEventListener
來清除沒有用到的事件,並釋放記憶體。
計時器未清除
與事件監聽類似的另一個常見的問題,是在使用計時器時導致的記憶體洩漏。例如:
setInterval(function() {
const someData = getData();
const node = document.getElementById('explainNode');
if(node) {
// 每 10 秒定時把 HTML 設定成拉取到的資料
node.innerHTML = JSON.stringify(someData));
}
}, 10000);
上面這種案例是透過 setInterval
來註冊某個定時觸發的定時器,除非很確定要讓這個定時器一直運行,不然需要透過 clearInterval
來清除,否則 someData
就會一直浪費記憶體的空間。
閉包
在 什麼是閉包 (Closure)? 一文當中,我們有談到閉包的使用,以及使用閉包的好處。在多數狀況下,使用閉包沒有什麼問題,但是要注意,由於閉包會讓內部函式記得外部的變數,這可能會造成變數常駐在記憶體當中,如果使用過多可能會造成記憶體洩漏需要小心使用。
Map 與 Set 的使用
在 請解釋 Set、Map、WeakSet 和 WeakMap 的區別? 一文當中,我們有談到 Map 與 Set 在 JavaScript 當中,都是強引用的,假如使用 Set,即使某個被存入的值,在其他地方已經沒有被引用,該值仍會存在於 Set 或 Map 當中,不會被垃圾回收。
多數情況下,在開發時用 Set 或 Map 即可,但是如果希望確保當 Set 與 Map 不再使用後,裡面的值一定被垃圾回收,那這時就會推薦用 WeakSet 或 WeakMap。
其他要注意的記憶體相關問題
除了上面提到的各類記憶體洩漏的潛在狀況外,在寫程式時,有一些點要特別注意,因為一不小心可能也會造成記憶體洩漏。其中包含:
循環引用 (circular references):循環引用是指當某個物件的屬性中,引用了另一個物件,而被引用的物件的屬性中,也引用了原物件。在這種狀況下,因為物件都仍被引用,就不會被垃圾回收,會有記憶體洩漏的風險。
不必要的
console
方法呼叫:許多剛接觸前端開發的人,可能經常會在寫程式時忘記移除console
的方法呼叫 (例如console.log
或console.error
)。因為這些方法印出的東西,在垃圾回收時不會被回收掉,因此也會佔用記憶體空間。在開發時使用沒問題,但是要部署到線上時,要記得清除掉。
閱讀更多記憶體洩漏相關內容
關於前端記憶體洩漏這個主題,如果你有想更深入了解,包含了解記憶體洩漏會怎麼樣、JavaScript 的記憶體管理如何運作,以及如何排查記憶體洩漏問題,我們在 E+ 有寫一篇更詳細版本的內容。
E+ 的詳細介紹可以在這看到 https://explainthis.io/zh-hant/e-plus
[本期推薦]
先前 Pragmatic Engineer 作者分享的推文,談到他大學時教授透過一個案例,讓課堂中的學生學會輸入框淨化 (sanitization) 的概念。該文讓我們想到考量極端案例 (edge case) 是工程師工作中,非常重要的要點,於是寫了一篇短文聊這主題 (連結)
Cursor 是近期很多工程師都在用的 AI 驅動 IDE,這週看到 Cursor 團隊自己用的提示詞,覺得非常酷,推薦參考 (連結)
最近讀了被說是邁向主任工程師必讀的《Staff Engineer: Leadership beyond the management track》一書,讀到一個在講提攜 (sponsorship) 的章節,覺得特別有感,於是寫下一點反思心得 (連結)。
先前看到 Jeff Dean 分享圖靈獎得主 (同時是 Google 的傑出工程師、前加州大學柏克萊分校教授) David Patterson 在業界 50 年的 16 點人生體悟,其中非常多點讓人感到非常有共鳴 (連結)
Notion 工程師 Sophie Alpert 最近在《Everyone is wrong about that Slack flowchart》一文中,把多年前 Slack 工程團隊分享的通知系統流程圖,簡化得非常一同,藉此說明系統流程圖可以不用畫得那麼複雜 (連結)
近期在社群被廣傳的《How WebSockets cost us $1M on our AWS bill》談了如何透過重新架構,大幅降低花費,寫得很精彩 (連結)
前陣子讀到《Hacking cars in JavaScript》,除了覺得很新奇外,再次感到 JavaScript 現在真的無所不在,不只是瀏覽器上,在智慧電視、車載系統等也都有 JavaScript 的足跡 (連結)