国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
一文帶你了解如何排查內(nèi)存泄漏導(dǎo)致的頁面卡頓現(xiàn)象
腳本之家
,與百萬開發(fā)者在一起

作者 | 零一0101
來源 | 前端印象(ID: Lpyexplore)

不知道在座的各位有沒有被問到過這樣一個問題:如果頁面卡頓,你覺得可能是什么原因造成的?有什么辦法鎖定原因并解決嗎?

這是一個非常寬泛而又有深度的問題,他涉及到很多的頁面性能優(yōu)化問題,我依稀還記得當(dāng)初面試被問到這個問題時我是這么回答的:

  1. 先會檢查是否是網(wǎng)絡(luò)請求太多,導(dǎo)致數(shù)據(jù)返回較慢,可以適當(dāng)做一些緩存
  2. 也有可能是某塊資源的 bundle 太大,可以考慮拆分一下
  3. 然后排查一下 js 代碼,是不是某處有過多循環(huán)導(dǎo)致占用主線程時間過長
  4. 瀏覽器某幀渲染的東西太多,導(dǎo)致的卡頓
  5. 在頁面渲染過程中,可能有很多重復(fù)的重排重繪
  6. emmmmmm....不知道了

后來了解到了,感官上的長時間運行頁面卡頓也有可能是因為內(nèi)存泄漏引起的

-

內(nèi)存泄漏的定義


那什么是內(nèi)存泄漏呢?借助別的大佬給出的定義,內(nèi)存泄漏就是指由于疏忽或者程序的某些錯誤造成未能釋放已經(jīng)不再使用的內(nèi)存的情況。簡單來講就是假設(shè)某個變量占用 100M 的內(nèi)存,而你又用不到這個變量,但是這個變量沒有被手動的回收或自動回收,即仍然占用 100M 的內(nèi)存空間,這就是一種內(nèi)存的浪費,即內(nèi)存泄漏

-

JS 的數(shù)據(jù)存儲


JavaScript 的內(nèi)存空間分為棧內(nèi)存堆內(nèi)存,前者用來存放一些簡單變量,后者用來存放復(fù)雜對象

  • 簡單變量指的是 JS 的基本數(shù)據(jù)類型,例如:String、Number、Boolean、null、undefined、Symbol、BigInt
  • 復(fù)雜對象指的是 JS 的引用數(shù)據(jù)類型,例如:Object、Array、Function...

-

JS 垃圾回收機制


根據(jù)內(nèi)存泄漏的定義,有些變量或數(shù)據(jù)不再被使用或不需要了,那么它就是垃圾變量或垃圾數(shù)據(jù),如果其一直保存在內(nèi)存中,最終可能會導(dǎo)致內(nèi)存占用過多的情況。那么此時就需要對這些垃圾數(shù)據(jù)進行回收,這里引入了垃圾回收機制的概念
垃圾回收的機制分為手動自動兩種
例如 C/C++ 采用的就是手動回收的機制,即先用代碼為某個變量分配一定的內(nèi)存,然后在不需要了后,再用代碼手動釋放掉內(nèi)存
而 JavaScript 采用的則是自動回收的機制,即我們不需要關(guān)心何時為變量分配多大的內(nèi)存,也不需要關(guān)心何時去釋放內(nèi)存,因為這一切都是自動的。但這不表示我們不需要關(guān)心內(nèi)存的管理?。。?!否則也不會有本文討論的內(nèi)存泄露了
接下來就講一下 JavaScript 的垃圾回收機制
通常全局狀態(tài)(window)下的變量是不會被自動回收的,所以我們來討論一下局部作用域下的內(nèi)存回收情況
function fn1 ({
    let a = {
        name'零一'
    }

    let b = 3

    function fn2({
        let c = [123]
    }

    fn2()

    return a
}

let res = fn1()
以上代碼的調(diào)用棧如下圖所示:

圖中左側(cè)為空間,用于存放一些執(zhí)行上下文和基本類型數(shù)據(jù);右側(cè)為堆空間,用于存放一些復(fù)雜對象數(shù)據(jù)

當(dāng)代碼執(zhí)行到 fn2() 時,??臻g內(nèi)的執(zhí)行上下文從上往下依次是 fn2 函數(shù)執(zhí)行上下文 => fn1 函數(shù)執(zhí)行上下文 => 全局執(zhí)行上下文
待 fn2 函數(shù)內(nèi)部執(zhí)行完畢以后,就該退出 fn2 函數(shù)執(zhí)行上下文了,即箭頭向下移動,此時 fn2 函數(shù)執(zhí)行上下文會被清除并釋放棧內(nèi)存空間,如圖所示:

待 fn1函數(shù)內(nèi)部執(zhí)行完畢以后,就該退出 fn1函數(shù)執(zhí)行上下文了,即箭頭再向下移動,此時 fn1函數(shù)執(zhí)行上下文會被清除并釋放相應(yīng)的棧內(nèi)存空間,如圖所示:

此時處于全局的執(zhí)行上下文中。JavaScript 的垃圾回收器會每隔一段時間遍歷調(diào)用棧,假設(shè)此時觸發(fā)了垃圾回收機制,當(dāng)遍歷調(diào)用棧時發(fā)現(xiàn)變量 b 和變量 c 沒有被任何變量所引用,所以認(rèn)定它們是垃圾數(shù)據(jù)并給它們打上標(biāo)記。因為fn1函數(shù)執(zhí)行完后將變量 a 返回了出去,并存儲在全局變量 res 中,所以認(rèn)定其為活動數(shù)據(jù)并打上相應(yīng)標(biāo)記。待空閑時刻會將標(biāo)記上垃圾數(shù)據(jù)的變量給全部清除掉,釋放相應(yīng)的內(nèi)存,如圖所示:


從這我們得出幾點結(jié)論:
  1. JavaScript 的垃圾回收機制是自動執(zhí)行的,并且會通過標(biāo)記來識別并清除垃圾數(shù)據(jù)
  2. 在離開局部作用域后,若該作用域內(nèi)的變量沒有被外部作用域所引用,則在后續(xù)會被清除

補充:JavaScript 的垃圾回收機制有著很多的步驟,上述只講到了標(biāo)記-清除,其實還有其它的過程,這里簡單介紹一下就不展開討論了。例如:標(biāo)記-整理,在清空部分垃圾數(shù)據(jù)后釋放了一定的內(nèi)存空間后會可能會留下大面積的不連續(xù)內(nèi)存片段,導(dǎo)致后續(xù)可能無法為某些對象分配連續(xù)內(nèi)存,此時需要整理一下內(nèi)存空間;交替執(zhí)行,因為 JavaScript 是運行在主線程上的,所以執(zhí)行垃圾回收機制時會暫停 js 的運行,若垃圾回收執(zhí)行時間過長,則會給用戶帶來明顯的卡頓現(xiàn)象,所以垃圾回收機制會被分成一個個的小任務(wù),穿插在js任務(wù)之中,即交替執(zhí)行,盡可能得保證不會帶來明顯的卡頓感

-

Chrome devTools 查看內(nèi)存情況


在了解一些常見的內(nèi)存泄漏的場景之前,先簡單介紹一下如何使用 Chrome 的開發(fā)者工具來查看js內(nèi)存情況

首先打開 Chrome 的無痕模式,這樣做的目的是為了屏蔽掉 Chrome 插件對我們之后測試內(nèi)存占用情況的影響

然后打開開發(fā)者工具,找到 Performance 這一欄,可以看到其內(nèi)部帶著一些功能按鈕,例如:開始錄制按鈕;刷新頁面按鈕;清空記錄按鈕;記錄并可視化js內(nèi)存、節(jié)點、事件監(jiān)聽器按鈕;觸發(fā)垃圾回收機制按鈕等等

簡單錄制一下百度頁面,看看我們能獲得什么,如下動圖所示:

從上圖中我們可以看到,在頁面從零到加載完成這個過程中 JS Heap(js堆內(nèi)存)、documents(文檔)、Nodes(DOM節(jié)點)、Listeners(監(jiān)聽器)、GPU memory(GPU內(nèi)存)的最低值、最高值以及隨時間的走勢曲線,這也是我們主要關(guān)注的點
再來看看開發(fā)者工具中的 Memory 一欄,其主要是用于記錄頁面堆內(nèi)存的具體情況以及js堆內(nèi)存隨加載時間線動態(tài)的分配情況
堆快照就像照相機一樣,能記錄你當(dāng)前頁面的堆內(nèi)存情況,每快照一次就會產(chǎn)生一條快照記錄,如圖所示:
如上圖所示,剛開始執(zhí)行了一次快照,記錄了當(dāng)時堆內(nèi)存空間占用為 13.9MB,然后我們點擊了頁面中某些按鈕,又執(zhí)行一次快照,記錄了當(dāng)時堆內(nèi)存空間占用為 13.4MB。并且點擊對應(yīng)的快照記錄,能看到當(dāng)時所有內(nèi)存中的變量情況(結(jié)構(gòu)、占總占用內(nèi)存的百分比...)
然后我們還可以看一下頁面動態(tài)的內(nèi)存變化情況,如圖所示:
在開始記錄后,我們可以看到圖中右上角有起伏的藍色與灰色的柱形圖,其中藍色表示當(dāng)前時間線下占用著的內(nèi)存;灰色表示之前占用的內(nèi)存空間已被清除釋放。
從上圖過程來看,我們可以看到剛開始處于的 tab 所對應(yīng)顯示的頁面中占用了一定的堆內(nèi)存空間,成藍色柱形,在點擊別的 tab 后,原 tab 對應(yīng)的內(nèi)容消失,并且原來藍色的柱形變成灰色(表示原占用的內(nèi)存空間得到了釋放),同時新 tab 所對應(yīng)顯示的頁面也占用了一定的堆內(nèi)存空間。因此后續(xù)我們就可以針對這個圖來查看內(nèi)存的占用與清除情況

-

內(nèi)存泄漏的場景

那么到底有哪些情況會出現(xiàn)內(nèi)存泄漏的情況呢?這里列舉了常見的幾種:
  1. 閉包使用不當(dāng)引起內(nèi)存泄漏
  2. 全局變量
  3. 分離的 DOM 節(jié)點
  4. 控制臺的打印
  5. 遺忘的定時器
接下來介紹一下各種情況,并嘗試用剛才講到的兩種方法來捕捉問題所在

5.1 閉包使用不當(dāng)

文章開頭的例子中,在退出 fn1函數(shù)執(zhí)行上下文后,該上下文中的變量 a 本應(yīng)被當(dāng)作垃圾數(shù)據(jù)給回收掉,但因 fn1函數(shù)最終將變量 a 返回并賦值給全局變量res,其產(chǎn)生了對變量 a 的引用,所以變量 a 被標(biāo)記為活動變量并一直占用著相應(yīng)的內(nèi)存,假設(shè)變量 res 后續(xù)用不到,這就算是一種閉包使用不當(dāng)?shù)睦?/span>
接下來嘗試使用 Performance和Memory 來查看一下閉包導(dǎo)致的內(nèi)存泄漏問題,為了使內(nèi)存泄漏的結(jié)果更加明顯,我們稍微改動一下文章開頭的例子,代碼如下:
<button onclick='myClick()'>執(zhí)行fn1函數(shù)</button>
<script>
    function fn1 ({
        let a = new Array(10000)  // 這里設(shè)置了一個很大的數(shù)組對象

        let b = 3

        function fn2({
            let c = [123]
        }

        fn2()

        return a
    }

    let res = []  

    function myClick({
        res.push(fn1())
    }
</script>
設(shè)置了一個按鈕,每次執(zhí)行就會將 fn1函數(shù)的返回值添加到全局?jǐn)?shù)組變量 res中,是為了能在 performacne 的曲線圖中看出效果,如圖所示:
在每次錄制開始時手動觸發(fā)一次垃圾回收機制,這是為了確認(rèn)一個初始的堆內(nèi)存基準(zhǔn)線,便于后面的對比,然后我們點擊了幾次按鈕,即往全局?jǐn)?shù)組變量 res 中添加了幾個比較大的數(shù)組對象,最后再觸發(fā)一次垃圾回收,發(fā)現(xiàn)錄制結(jié)果的 JS Heap 曲線剛開始成階梯式上升的,最后的曲線的高度比基準(zhǔn)線要高,說明可能是存在內(nèi)存泄漏的問題
在得知有內(nèi)存泄漏的情況存在時,我們可以改用 Memory 來更明確得確認(rèn)問題和定位問題
首先可以用 Allocation instrumentation on timeline 來確認(rèn)問題,如下圖所示:
在我們每次點擊按鈕后,動態(tài)內(nèi)存分配情況圖上都會出現(xiàn)一個藍色的柱形,并且在我們觸發(fā)垃圾回收后,藍色柱形都沒變成灰色柱形,即之前分配的內(nèi)存并未被清除
所以此時我們就可以更明確得確認(rèn)內(nèi)存泄漏的問題是存在的了,接下來就精準(zhǔn)定位問題,可以利用 Heap snapshot 來定位問題,如圖所示:
第一次先點擊快照記錄初始的內(nèi)存情況,然后我們多次點擊按鈕后再次點擊快照,記錄此時的內(nèi)存情況,發(fā)現(xiàn)從原來的 1.1M 內(nèi)存空間變成了 1.4M 內(nèi)存空間,然后我們選中第二條快照記錄,可以看到右上角有個All objects的字段,其表示展示的是當(dāng)前選中的快照記錄所有對象的分配情況,而我們想要知道的是第二條快照與第一條快照的區(qū)別在哪,所以選擇 Object allocated between Snapshot1 and Snapshot2,即展示第一條快照和第二條快照存在差異的內(nèi)存對象分配情況,此時可以看到 Array 的百分比很高,初步可以判斷是該變量存在問題,點擊查看詳情后就能查看到該變量對應(yīng)的具體數(shù)據(jù)了
以上就是一個判斷閉包帶來內(nèi)存泄漏問題并簡單定位的方法了

5.2 全局變量

全局的變量一般是不會被垃圾回收掉的,在文章開頭也提到過了。當(dāng)然這并不是說變量都不能存在全局,只是有時候會因為疏忽而導(dǎo)致某些變量流失到全局,例如未聲明變量,卻直接對某變量進行賦值,就會導(dǎo)致該變量在全局創(chuàng)建,如下所示:
function fn1({
    // 此處變量name未被聲明
    name = new Array(99999999)
}

fn1()
此時這種情況就會在全局自動創(chuàng)建一個變量 name,并將一個很大的數(shù)組賦值給 name,又因為是全局變量,所以該內(nèi)存空間就一直不會被釋放
解決辦法的話,自己平時要多加注意,不要在變量未聲明前賦值,或者也可以開啟嚴(yán)格模式,這樣就會在不知情犯錯時,收到報錯警告,例如:
function fn1({
    'use strict';
    name = new Array(99999999)
}

fn1()

5.3 分離的 DOM 節(jié)點

什么叫 DOM 節(jié)點?假設(shè)你手動移除了某個 dom 節(jié)點,本應(yīng)釋放該 dom 節(jié)點所占用的內(nèi)存,但卻因為疏忽導(dǎo)致某處代碼仍對該被移除節(jié)點有引用,最終導(dǎo)致該節(jié)點所占內(nèi)存無法被釋放,例如這種情況:
<div id='root'>
    <div class='child'>我是子元素</div>
    <button>移除</button>
</div>
<script>

    let btn = document.querySelector('button')
    let child = document.querySelector('.child')
    let root = document.querySelector('#root')
    
    btn.addEventListener('click'function({
        root.removeChild(child)
    })

</script>
該代碼所做的操作就是點擊按鈕后移除.child 的節(jié)點,雖然點擊后,該節(jié)點確實從 dom 被移除了,但全局變量 child 仍對該節(jié)點有引用,所以導(dǎo)致該節(jié)點的內(nèi)存一直無法被釋放,可以嘗試用 Memory 的快照功能來檢測一下,如圖所示:
同樣的先記錄一下初始狀態(tài)的快照,然后點擊移除按鈕后,再點擊一次快照,此時內(nèi)存大小我們看不出什么變化,因為移除的節(jié)點占用的內(nèi)存實在太小了可以忽略不計,但我們可以點擊第二條快照記錄,在篩選框里輸入 detached,于是就會展示所有脫離了卻又未被清除的節(jié)點對象
解決辦法如下圖所示:
<div id='root'>
    <div class='child'>我是子元素</div>
    <button>移除</button>
</div>
<script>
    let btn = document.querySelector('button')

    btn.addEventListener('click'function({  
        let child = document.querySelector('.child')
        let root = document.querySelector('#root')

        root.removeChild(child)
    })

</script>
改動很簡單,就是將對.child 節(jié)點的引用移動到了 click 事件的回調(diào)函數(shù)中,那么當(dāng)移除節(jié)點并退出回調(diào)函數(shù)的執(zhí)行上文后就會自動清除對該節(jié)點的引用,那么自然就不會存在內(nèi)存泄漏的情況了,我們來驗證一下,如下圖所示:
結(jié)果很明顯,這樣處理過后就不存在內(nèi)存泄漏的情況了

5.4 控制臺的打印

控制臺的打印也會造成內(nèi)存泄漏嗎????是的呀,如果瀏覽器不一直保存著我們打印對象的信息,我們?yōu)楹文茉诿看未蜷_控制的 Console 時看到具體的數(shù)據(jù)呢?先來看一段測試代碼:
<button>按鈕</button>
<script>
    document.querySelector('button').addEventListener('click'function({
        let obj = new Array(1000000)

        console.log(obj);
    })
</script>
我們在按鈕的點擊回調(diào)事件中創(chuàng)建了一個很大的數(shù)組對象并打印,用 performance 來驗證一下:
開始錄制,先觸發(fā)一次垃圾回收清除初始的內(nèi)存,然后點擊三次按鈕,即執(zhí)行了三次點擊事件,最后再觸發(fā)一次垃圾回收。查看錄制結(jié)果發(fā)現(xiàn) JS Heap 曲線成階梯上升,并且最終保持的高度比初始基準(zhǔn)線高很多,這說明每次執(zhí)行點擊事件創(chuàng)建的很大的數(shù)組對象 obj 都因為 console.log 被瀏覽器保存了下來并且無法被回收
接下來注釋掉 console.log,再來看一下結(jié)果:
<button>按鈕</button>
<script>
    document.querySelector('button').addEventListener('click'function({
        let obj = new Array(1000000)

        // console.log(obj);
    })
</script>
performance 如圖所示:
可以看到?jīng)]有打印以后,每次創(chuàng)建的 obj 都立馬被銷毀了,并且最終觸發(fā)垃圾回收機制后跟初始的基準(zhǔn)線同樣高,說明已經(jīng)不存在內(nèi)存泄漏的現(xiàn)象了
其實同理,console.log 也可以用 Memory 來進一步驗證
  • 未注釋 console.log
  • 注釋掉了 console.log
最后簡單總結(jié)一下:在開發(fā)環(huán)境下,可以使用控制臺打印便于調(diào)試,但是在生產(chǎn)環(huán)境下,盡可能得不要在控制臺打印數(shù)據(jù)。所以我們經(jīng)常會在代碼中看到類似如下的操作:
// 如果在開發(fā)環(huán)境下,打印變量obj
if(isDev) {
    console.log(obj)
}
這樣就避免了生產(chǎn)環(huán)境下無用的變量打印占用一定的內(nèi)存空間,同樣的除了 console.log 之外,console.error、console.info、console.dir等等都不要在生產(chǎn)環(huán)境下使用

5.5 遺忘的定時器

其實定時器也是平時很多人會忽略的一個問題,比如定義了定時器后就再也不去考慮清除定時器了,這樣其實也會造成一定的內(nèi)存泄漏。來看一個代碼示例:
<button>開啟定時器</button>
<script>

    function fn1({
        let largeObj = new Array(100000)

        setInterval(() => {
            let myObj = largeObj
        }, 1000)
    }

    document.querySelector('button').addEventListener('click'function({
        fn1()
    })
</script>
這段代碼是在點擊按鈕后執(zhí)行 fn1函數(shù),fn1函數(shù)內(nèi)創(chuàng)建了一個很大的數(shù)組對象 largeObj,同時創(chuàng)建了一個 setInterval 定時器,定時器的回調(diào)函數(shù)只是簡單的引用了一下變量 largeObj,我們來看看其整體的內(nèi)存分配情況吧:
按道理來說點擊按鈕執(zhí)行 fn1函數(shù)后會退出該函數(shù)的執(zhí)行上下文,緊跟著函數(shù)體內(nèi)的局部變量應(yīng)該被清除,但圖中 performance 的錄制結(jié)果顯示似乎是存在內(nèi)存泄漏問題的,即最終曲線高度比基準(zhǔn)線高度要高,那么再用 Memory 來確認(rèn)一次:
在我們點擊按鈕后,從動態(tài)內(nèi)存分配的圖上看到出現(xiàn)一個藍色柱形,說明瀏覽器為變量 largeObj 分配了一段內(nèi)存,但是之后這段內(nèi)存并沒有被釋放掉,說明的確存在內(nèi)存泄漏的問題,原因其實就是因為 setInterval 的回調(diào)函數(shù)內(nèi)對變量 largeObj 有一個引用關(guān)系,而定時器一直未被清除,所以變量 largeObj 的內(nèi)存也自然不會被釋放
那么我們?nèi)绾蝸斫鉀Q這個問題呢,假設(shè)我們只需要讓定時器執(zhí)行三次就可以了,那么我們可以改動一下代碼:
<button>開啟定時器</button>
<script>
    function fn1({
        let largeObj = new Array(100000)
        let index = 0

        let timer = setInterval(() => {
            if(index === 3) clearInterval(timer);
            let myObj = largeObj
            index ++
        }, 1000)
    }

    document.querySelector('button').addEventListener('click'function({
        fn1()
    })
</script>
現(xiàn)在我們再通過 performance 和 memory 來看看還不會存在內(nèi)存泄漏的問題
  • performance
這次的錄制結(jié)果就能看出,最后的曲線高度和初始基準(zhǔn)線的高度一樣,說明并沒有內(nèi)存泄漏的情況
  • memory
這里做一個解釋,圖中剛開始出現(xiàn)的藍色柱形是因為我在錄制后刷新了頁面,可以忽略;然后我們點擊了按鈕,看到又出現(xiàn)了一個藍色柱形,此時就是為fn1函數(shù)中的變量 largeObj 分配了內(nèi)存,3s后該內(nèi)存又被釋放了,即變成了灰色柱形。所以我們可以得出結(jié)論,這段代碼不存在內(nèi)存泄漏的問題
簡單總結(jié)一下:大家在平時用到了定時器,如果在用不到定時器后一定要清除掉,否則就會出現(xiàn)本例中的情況。除了 setTimeout 和 setInterval,其實瀏覽器還提供了一個API也可能就存在這樣的問題,那就是requestAnimationFrame

-

總結(jié)


在項目過程中,如果遇到了某些性能問題可能跟內(nèi)存泄漏有關(guān)時,就可以參照本文列舉的 5 種情況去排查,一定能找到問題所在并給到解決辦法的。
雖然 JavaScript 的垃圾回收是自動的,但我們有時也是需要考慮要不要手動清除某些變量的內(nèi)存占用的,例如你明確某個變量在一定條件下再也不需要,但是還會被外部變量引用導(dǎo)致內(nèi)存無法得到釋放時,你可以用 null 對該變量重新賦值就可以在后續(xù)垃圾回收階段釋放該變量的內(nèi)存了。

這個挑戰(zhàn),是年輕人不可逃避的!

觀看視頻內(nèi)容

本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
【前端進階之路】內(nèi)存基本知識
使用Chrome DevTools的Timeline和Profiles提高Web應(yīng)用程序的性能
淺談 JS 內(nèi)存機制
JavaScript 之 作用域
分析內(nèi)存泄露,一個前端工程師需要掌握的技能
使用 Chrome Dev tools 分析應(yīng)用的內(nèi)存泄漏問題
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服