HackMD
  • Beta
    Beta  Get a sneak peek of HackMD’s new design
    Turn on the feature preview and give us feedback.
    Go → Got it
      • Create new note
      • Create a note from template
    • Beta  Get a sneak peek of HackMD’s new design
      Beta  Get a sneak peek of HackMD’s new design
      Turn on the feature preview and give us feedback.
      Go → Got it
      • Sharing Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • 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
      • More (Comment, Invitee)
      • Publishing
        Please check the box to agree to the Community Guidelines.
        Everyone on the web can find and read all notes of this public team.
        After the note is published, everyone on the web can find and read this note.
        See all published notes on profile page.
      • Commenting Enable
        Disabled Forbidden Owners Signed-in users Everyone
      • Permission
        • Forbidden
        • Owners
        • Signed-in users
        • Everyone
      • Invitee
      • No invitee
      • Options
      • Versions and GitHub Sync
      • Transfer ownership
      • Delete this note
      • Template
      • Save as template
      • Insert from template
      • Export
      • Dropbox
      • Google Drive Export to Google Drive
      • Gist
      • Import
      • Dropbox
      • Google Drive Import from Google Drive
      • Gist
      • Clipboard
      • Download
      • Markdown
      • HTML
      • Raw HTML
    Menu Sharing Create Help
    Create Create new note Create a note from template
    Menu
    Options
    Versions and GitHub Sync Transfer ownership Delete this note
    Export
    Dropbox Google Drive Export to Google Drive Gist
    Import
    Dropbox Google Drive Import from Google Drive Gist Clipboard
    Download
    Markdown HTML Raw HTML
    Back
    Sharing
    Sharing Link copied
    /edit
    View mode
    • Edit mode
    • View mode
    • Book mode
    • Slide mode
    Edit mode View mode Book mode Slide mode
    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
    More (Comment, Invitee)
    Publishing
    Please check the box to agree to the Community Guidelines.
    Everyone on the web can find and read all notes of this public team.
    After the note is published, everyone on the web can find and read this note.
    See all published notes on profile page.
    More (Comment, Invitee)
    Commenting Enable
    Disabled Forbidden Owners Signed-in users Everyone
    Permission
    Owners
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Invitee
    No invitee
       owned this note    owned this note      
    Published Linked with GitHub
    Like1 BookmarkBookmarked
    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

    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 lost their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template is not available.


    Upgrade

    All
    • All
    • Team
    No template found.

    Create custom template


    Upgrade

    Delete template

    Do you really want to delete this template?

    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

    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

    Tutorials

    Book Mode Tutorial

    Slide Mode Tutorial

    YAML Metadata

    Contacts

    Facebook

    Twitter

    Feedback

    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

    Versions and GitHub Sync

    Sign in to link this note to GitHub Learn more
    This note is not linked with GitHub Learn more
     
    Add badge Pull Push GitHub Link Settings
    Upgrade now

    Version named by    

    More Less
    • Edit
    • Delete

    Note content is identical to the latest version.
    Compare with
      Choose a version
      No search result
      Version not found

    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. Learn more

         Sign in to GitHub

        HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.

        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
        Available push count

        Upgrade

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Upgrade

        Danger Zone

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

        Syncing

        Push failed

        Push successfully