Joy Hung
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
1
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# Causes of Memory Leaks in JavaScript and How to Avoid Them ## What is Memory leaks **Reference chain** * Browser 會將 objects 存放在 heap memory 中,從 root(window/global) 藉由 reference chain 可到達這些 objects。 ![](https://i.imgur.com/EW6NuKX.png) **Garbage Collection(簡稱 GC)** * Garbage Collector 是 JavaScript 引擎中的一種 background process,工作是鑑別無法藉由 reference chain 到達的 objects 將其刪除,並回收相對應的記憶體空間 下圖中 Object 4 會從記憶體中移除 ![](https://i.imgur.com/76FvcAw.png) **Mark and Sweep 回收策略** JavaScript最常用垃圾回收策略是" 標記清理(mark-and-sweep) " 策略概念:遍歷空間下所有的物件,並標記有被引用的並且最終可以到達 根(window/global) 的物件。在 GC 階段,清除沒有標記的物件。 ![](https://i.imgur.com/cJ0R28a.gif) **Memory Leak** * 當 object 本該被 garbage collector 被清掉,卻由於疏忽或錯誤,導致本應該被 GC 清除的 object 意外被引用而維持可被 reference chain 訪問的狀態,就會發生記憶體洩漏。 * 當多餘的 object 存在記憶體中,會造成記憶體浪費,嚴重的話可能會導致程式效能變慢甚至網頁 crash。 ## JavaScript 導致 Memory Leaks 常見的 6 種情形 ### 1.不當存取全域變數 全域變數不會被 GC 回收。在非嚴格模式下,需避免以下錯誤: * 給未宣告的變數賦值,會讓區域變數變成全域變數 ```javascript= // wrong function createGlobalVariables() { leaking1 = '變成全域性變數'; // 如果作用域內沒有宣告變數,卻賦值給該變數,JavaScript 會自動幫我們在全域宣告一個全域變數 }; createGlobalVariables(); window.leaking1; // '變成全域性變量了' => 在瀏覽器下的全域物件是 window ``` * 使用指向全域性物件的 this,會讓區域變數變成全域變數 ```javascript= // wrong function createGlobalVariables() { this.leaking2 = '這也是全域性變數'; }; createGlobalVariables(); // => 直接呼叫函式的情況下,this 會指向全域 window.leaking2; // '這也是全域性變數' ``` **如何避免** 採用嚴格模式("use strict").上述例子在嚴格模式下會爆錯,避免 memory leak 產生 ### 2.Closures 閉包 一般的函式作用域變數在函式執行完後會被清理. 閉包讓我們可以從 inner 函式訪問 outer 函式 scope 的變數,此特性會讓該變數一直處於被引用狀態,不會被 GC 回收。 ```javascript= // wrong function outer() { const potentiallyHugeArray = []; return function inner() { potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable console.log('Hello'); console.log('potentiallyHugeArray', potentiallyHugeArray); }; }; const sayHello = outer(); // contains definition of the function inner function repeat(fn, num) { for (let i = 0; i < num; i++){ fn(); } } repeat(sayHello, 10); // 每次呼叫 sayHello 都會新增 'Hello' 到potentiallyHugeArray // now imagine repeat(sayHello, 100000) ``` **如何避免** 在使用閉包時需要清楚知道 * 何時創建了閉包以及閉包保留了哪些 Objects * 閉包的預期壽命以及使用情形,尤其是作為 callback 來使用的時候 ### 3.Timers 定時器 在 setTimeout 或 setInterval 的 callback 函式中引用某些物件,是防止被 GC 回收的常見做法。 下面的例子中,data 物件只能在 timer 清掉後被 GC 回收。但因為沒有拿到 setInterval return 的定時器 ID,也就沒辦法用程式碼清除這個 timer。 雖然 data.hugeString 完全沒被使用,也會一直保留在記憶體中。 ```javascript= // wrong function setCallback() { const data = { counter: 0, hugeString: new Array(100000).join('x') }; return function cb() { data.counter++; // data 物件現在已經屬於 callback 函式的作用域 console.log(data.counter); } } setInterval(setCallback(), 1000); // 無法停止定時器 ``` **如何避免** 對於使用壽命未定義或不確定的 callback 函式,我們應該: * 留意被 timer 的 callback 所參考的物件 * 必要時使用 timer return 的定時器 ID,丟進 clearTimeout 或 clearInterval 以清除 timer。 ```javascript= // right function setCallback() { // 分開定義變數 let counter = 0; const hugeString = new Array(100000).join('x'); // setCallback return 後即被回收 return function cb() { counter++; // 只剩 counter 位於 callback 函式作用域 console.log(counter); } } const timerId = setInterval(setCallback(), 1000); // 儲存定時器 ID // 執行某些操作 ... clearInterval(timerId); // 清除定時器,ex 按完按鈕後清除 ``` ### 4.Event listeners 事件監聽器 事件監聽器會阻止其作用域內的變數被 GC 回收,事件監聽器會一直處於 active,直到 1. 使用 removeEventListener() 移除該 event listers 2. 移除與其關聯的 DOM 元素 ```javascript= // wrong const hugeString = new Array(100000).join('x'); document.addEventListener('keyup', function() { // 匿名監聽器無法移除 doSomething(hugeString); // hugeString 會一直處於 callback 函式的作用域內 }); ``` 上面例子中 1. 事件監聽器用了匿名函式,沒法用 removeEventListener()移除 2. 也無法刪除 document元素 因此事件 callback 函式內的變數會一直保留,就算我們只想觸發一次事件。 **如何避免** 當事件監聽器不再需要時,使用具名函式方式得到其 reference,並且在 removeEventListener() 中解除事件監聽器跟關聯的 DOM 元素的連結 ```javascript= // right function listener() { doSomething(hugeString); } document.addEventListener('keyup', listener); document.removeEventListener('keyup', listener); ``` 如果事件監聽器只需要執行一次, addEventListener()可以接受[第三個 optional 參數 {once: true}](https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener),監聽器函式會在事件觸發一次執行後自動移除(此時匿名函式也可以用此方式)。 ```javascript= // right document.addEventListener('keyup', function listener(){ doSomething(hugeString); }, {once: true}); // 執行一次後自動移除事件監聽器 ``` React 寫法 ```javascript= useEffect(() => { window.addEventListener('keyup', listener); return () => { window.removeEventListener('keyup', listener); }; },[]); ``` ### 5.Cache 快取 如果持續地往快取裡增加資料,沒有定時清除無用的物件,也沒有限制快取大小,那麼快取就會像滾雪球一樣越來越大。 Wrong Example: 使用 Map 資料結構來儲存快取 ```javascript= // wrong let user_1 = { name: "Peter", id: 12345 }; let user_2 = { name: "Mark", id: 54321 }; const mapCache = new Map(); function cache(obj){ if (!mapCache.has(obj)){ const value = `${obj.name} has an id of ${obj.id}`; mapCache.set(obj, value); // 添加 key return [value, 'computed']; } return [mapCache.get(obj), 'cached']; // 讀取 value } cache(user_1); // ['Peter has an id of 12345', 'computed'] cache(user_1); // ['Peter has an id of 12345', 'cached'] cache(user_2); // ['Mark has an id of 54321', 'computed'] console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321") user_1 = null; // removing the inactive user // Garbage Collector console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321") // first entry is still in cache ``` 當設定 key value pair 在 Map 的資料結構中,即使之後 key 被清空,原本的 key value pair 還是依然存在,因為 Map 中所有 key 和 value 會一直被引用,導致無法被 GC ![](https://i.imgur.com/v0sEm8B.png) 解決方案: 上述案例可以改使用 [WeakMap](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)。 WeakMap 是 ES6 新增的一種資料結構,它只用物件作為 key,並保持物件 key 的 weak reference,如果物件 key 被置空了,相關的 key value pair 會被 GC 自動回收。 ```javascript= // right let user_1 = { name: "Kayson", id: 12345 }; let user_2 = { name: "Jerry", id: 54321 }; const weakMapCache = new WeakMap(); function cache(obj){ // 程式碼跟前一個例子相同,只不過用的是 weakMapCache return [weakMapCache.get(obj), 'cached']; } cache(user_1); // ['Kayson has an id of 12345', 'computed'] cache(user_2); // ['Jerry has an id of 54321', 'computed'] console.log(weakMapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321"} user_1 = null; // Garbage Collector console.log(weakMapCache); // ((…) => "Jerry has an id of 54321") - 第一條記錄已被 GC 刪除 ``` ### 6.移除 DOM 節點 如果 DOM 節點被 JavaScript 程式碼引用,即使將該節點從 DOM three 移除,也不會被 GC 回收。 ```javascript= // wrong function createElement() { const div = document.createElement('div'); div.id = 'detached'; return div; } // 即使呼叫了deleteElement() ,全域變數 detachedDiv 依然儲存著 DOM 元素的 reference,所以無法被 GC 回收 const detachedDiv = createElement(); document.body.appendChild(detachedDiv); function deleteElement() { document.body.removeChild(document.getElementById('detached')); } deleteElement(); // Heap snapshot 顯示為 detached div#detached ``` **如何避免** 限制只能在 local scope 之內引用 DOM ```javascript= // right function createElement() {...} // 程式碼跟前一個例子相同 function appendElement() { // 在 local scope 之內引用 DOM const detachedDiv = createElement(); document.body.appendChild(detachedDiv); } appendElement(); function deleteElement() { document.body.removeChild(document.getElementById('detached')); } deleteElement(); ``` **結論** 了解 GC 可以幫助我們寫出比較不容易記憶體洩漏的程式碼。在開發上這並不是一個容易被找出來的問題,透過以上手法我們可以防範一些比較基本的錯誤,必要時也可以透過 Chrome dev tool 來除錯。 ### Reference 1. https://www.ditdot.hr/en/causes-of-memory-leaks-in-javascript-and-how-to-avoid-them 2. 通過【垃圾回收機制】的角度認識【Map與WeakMap】的區別 https://www.gushiciku.cn/pl/g4iM/zh-tw?fbclid=IwAR3_BMatJFYGTMtJbc331F4Iur1fyKTCuNQSmWHN5Ja2ftLLeO-qwP_JxT4 3. https://medium.com/starbugs/%E8%BA%AB%E7%82%BA-js-%E9%96%8B%E7%99%BC%E8%80%85-%E4%BD%A0%E4%B8%8D%E8%83%BD%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E8%A8%98%E6%86%B6%E9%AB%94%E7%AE%A1%E7%90%86%E6%A9%9F%E5%88%B6-d9db2fd66f8 4. https://ithelp.ithome.com.tw/articles/10214185

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully