【有價值】vue3內(nèi)存泄露問題定位過程

本文包含以下章節(jié):

  • 1、內(nèi)存泄露的排查點
  • 2、案例一:樹組件的每個節(jié)點增加hover事件導致內(nèi)存泄露
  • 3、案例二:表格組件的slot中使用表格行數(shù)據(jù)row作為內(nèi)聯(lián)函數(shù)的參數(shù)和動態(tài)判斷class的依據(jù)
  • 4、案例三:表格組件的slot中使用表格行數(shù)據(jù)row作為參數(shù)傳遞給子組件
  • 5、案例四:樹組件的子節(jié)點數(shù)據(jù)懶加載回來后,將數(shù)據(jù)綁定在當前點擊的父節(jié)點node上,此處直接修改treeData的一個節(jié)點數(shù)據(jù),導致舊的表格數(shù)據(jù)沒有釋放,出現(xiàn)內(nèi)存增長從而泄露
  • 6、工具使用:chrome性能監(jiān)控

寫在最后的總結(jié):寫完整個經(jīng)驗總結(jié)后,發(fā)現(xiàn)4個案例有一個共同點:el-table、el-tree等組件的點擊事件或插槽slot中如果給你返回了行數(shù)據(jù)row、列頭信息column、節(jié)點數(shù)據(jù)node等參數(shù),千萬不要直接修改他們。如果有修改的業(yè)務場景,將要修改的數(shù)據(jù)先深拷貝一份然后修改拷貝的數(shù)據(jù),所有的修改,想要響應式的反映在頁面上時,一定是整個重置treeData或者tableData,而不能重置他們中的某一個行數(shù)據(jù)或者一個節(jié)點node數(shù)據(jù)。

// =====正確
// 克隆row
let rowCopy = cloneDeep(row)
// 修改克隆的row
rowCopy.editableField={label: '想修改的值'}
// 克隆tableData
let tableDataCopy = cloneDeep(tableData)
// 修改克隆的tableData
tableDataCopy[index] = rowCopy 
// 重置tableData
tableData.value = [...tableDataCopy ]

// =====錯誤
row.editableField={label: '想修改的值'}  // 錯誤:直接修改row
tableData[index] = rowCopy  // 錯誤:直接修改tableData中的一行數(shù)據(jù)

一、內(nèi)存泄露的排查點

1、本次排查出的問題(每一個都對本次泄露問題有影響)

  • 樹組件的slot中,不要給每個節(jié)點綁定div,可以考慮在父節(jié)點上綁定一個div然后每次調(diào)整顯示位置
  • 樹組件的slot中,不要給每個節(jié)點綁定事件(如mouseenter、mouseleave、click),可以考慮用樣式替代或者在父節(jié)點上增加一次事件
  • element-dialog泄露:dialog嵌套虛擬樹或者大型數(shù)據(jù)的組件時,一定要配置destroyOnClose為true,并且配合下面兩點使用才有效:在對話框關(guān)閉時數(shù)據(jù)對象一定要置為null、對話框內(nèi)容區(qū)域的組件或dom,一定要用v-if,跟隨對話框關(guān)閉時銷毀內(nèi)部組件
  • 公共組件虛擬樹中,引用超大的國際化文件,導致內(nèi)存泄露(虛擬樹被嵌套在對話框中才會暴露該問題)
  • table、tree組件使用時,v-if的判斷中不能有data數(shù)據(jù)或data.length>0這樣的判斷
  • template中不要使用箭頭函數(shù),比如:<div @click='e => func(e, row.id)'>
  • template中函數(shù)的參數(shù),不要使用數(shù)據(jù)對象,比如:<div @click='e => func(scope.row)'>,因為vue渲染時一直使用該變量,導致dom元素無法釋放
  • 樣式類盡量提前定義好,不要使用數(shù)據(jù)作為參數(shù)來動態(tài)計算樣式類,如<div :class='{"aClass": scope.row.isA}'>。因為vue渲染時一直使用該變量,導致dom元素無法釋放
  • 閉包導致的泄露(后面補充案例:websocket封裝過程中,回調(diào)函數(shù)未釋放導致泄露)

補充概念:內(nèi)聯(lián)函數(shù)(inline function)在 Vue 模板中指的是直接在模板中定義的函數(shù)表達式,它們通常會導致性能問題和潛在的內(nèi)存泄漏。常見形式包括以下3種:

<!-- 1. 事件處理中的內(nèi)聯(lián)函數(shù) -->
<button @click="() => doSomething(item)">按鈕</button>

<!-- 2. 插值中的內(nèi)聯(lián)函數(shù) -->
<div>{{ item.value.toFixed(2) }}</div>

<!-- 3. 計算屬性中的內(nèi)聯(lián)邏輯 -->
<div :class="{ active: isActive(item.id) }"></div>

2、常見內(nèi)存泄露排查點

  • 全局變量在onUnmounted中置為null,完成清理,尤其是大型數(shù)據(jù)
  • 定時器在onUnmounted中置為null完成清理,如setTimeout和setInterval
  • dom和事件要在onBeforeUnmount中銷毀,變量在onUnmounted中銷毀
  • 動態(tài)添加的dom元素,要及時清理
  • 深度拷貝,用lodash-es的cloneDeep方法代替Json.parse(Json.stringfy(aa))
  • 用WeakMap 替代對象緩存字符串
// 不推薦
const cache = {};
function process(data) {
  cache[data.id] = JSON.stringify(data); // 無限增長
}
// 推薦
const cache = new WeakMap();
function process(obj) {
  const str = JSON.stringify(obj);
  cache.set(obj, str); // 隨對象自動回收
}
  • 數(shù)組 join 替代字符串拼接
  • 固定對象結(jié)構(gòu)
// 不推薦:創(chuàng)建大量不同形狀的對象
function createUser() {
  const obj = {};
  obj[Math.random().toString(36)] = true;
  return obj;
}

// 推薦始終使用相同屬性結(jié)構(gòu)
function createUser() {
  return {
    id: null,
    name: null,
    // 明確所有可能字段
  };
}
  • 使用 Map 替代動態(tài)對象
// 不推薦:同一變量賦值不同形狀對象
let obj;
if (Math.random() > 0.5) {
  obj = { a: 1 };
} else {
  obj = { b: 1, c: 2 }; // 不同shape
}

// 推薦
const obj = new Map();
obj.set('dynamicKey', true); // 不影響對象shape
  • 清除或重置store中的變量

二、問題現(xiàn)象以及定位過程:案例一

1、問題現(xiàn)象:頁面是左樹右表,樹組件使用的是我們自己基于el-tree封裝的組件,額外增加的功能是,鼠標hover事件會觸發(fā)節(jié)點的增刪改按鈕浮動框出現(xiàn),用來增刪改當前樹節(jié)點;同時,點擊樹節(jié)點后,右側(cè)表格內(nèi)容會更新。問題現(xiàn)象是,每次點擊樹節(jié)點,會發(fā)現(xiàn)內(nèi)存增長過快

界面截圖
左側(cè)樹放大

2、技術(shù)使用:vue3+element-plus

3、問題定位思路和過程:

1、排查點擊事件函數(shù)是否有dom的處理,沒有發(fā)現(xiàn)問題
2、排查template中的對事件的寫法,發(fā)現(xiàn)每個節(jié)點都綁定了mouseenter和mouseleave事件,用來顯示增刪改按鈕組。驗證后發(fā)現(xiàn),確實是這兩個鼠標事件導致內(nèi)存快速增長。判斷依據(jù)是,鼠標只需要在樹節(jié)點上滑動,并不需要點擊,就能看到內(nèi)存快速增長到500M
3、每一層節(jié)點上都綁定了該事件,并且使用v-show默認隱藏按鈕組div。實際業(yè)務上,只需要對最外出節(jié)點顯示按鈕組,其他子節(jié)點不需要


每個節(jié)點都綁定了mouseenter和mouseleave事件

4、分析:一、不應該給每個節(jié)點增加事件,應該替換成在父節(jié)點上增加一個事件,然后使用冒泡去處理業(yè)務邏輯,減少事件觸發(fā);或者采用hover樣式替代事件。二、不需要的節(jié)點上,不增加額外的事件,只給最外層增加即可;三、用v-if替換v-show,減少不必要的dom渲染。優(yōu)化后的代碼如下所示:

刪除事件綁定,使用v-if替換v-show
使用hover樣式替代事件

三、問題現(xiàn)象以及定位過程:案例二

1、問題現(xiàn)象:有一個表格,單元格中有一個按鈕,點擊按鈕會彈出對話框。對話框左邊是虛擬樹,右邊是表格

三個點的圓圈是按鈕,點擊能打開對話框

對話框中的內(nèi)容

2、技術(shù)使用:vue3+element-plus

3、問題定位思路和過程:

1、排查我們自己封裝的虛擬樹,沒有泄露
2、排查我們自己封裝的對話框,有泄漏:對話框內(nèi)嵌套5萬個div,關(guān)閉時不釋放dom,得出結(jié)論:我們封裝的對話框有泄漏
2、排查element的對話框,有泄漏:對話框內(nèi)嵌套5萬個div,關(guān)閉時不釋放dom,要配置destroyOnClose,并且5萬個div外嵌套一個總的div,總的div上用v-if=dialogVisble在關(guān)閉對話框時銷毀全部div。得出結(jié)論:element的對話框本身有泄漏,需要配置屬性,并且嵌套內(nèi)容要配合v-if;我們自己封裝的對話框,也要這樣修改
3、我們封裝的對話框中嵌套elment原生的虛擬樹,有泄漏,需要把虛擬樹的treeData對象在關(guān)閉對話框時置為空數(shù)組或者null。得出結(jié)論:element的對話框依然有泄漏,需要把treeData在關(guān)閉時置為空數(shù)組或者null
4、修改以上問題后,用我們封裝的對話框中嵌套我們封裝的虛擬樹,依然有泄露,二分法刪除我們封裝的虛擬樹組件的代碼,最后定位原因是,引用了一個超大的國際化文件得出結(jié)論:對話框嵌套我們封裝的虛擬樹有問題,要刪除國際化文件的引用
5、-------------至此,我們封裝的對話框嵌套我們封裝的虛擬樹,已經(jīng)沒有問題??梢曰氐絾栴}暴露點上了
6、點擊表格單元格內(nèi)的按鈕,打開對話框,有泄漏,刪除對話框,多個表格切換,仍然有內(nèi)存泄露,說明表格本身有泄漏。得出結(jié)論:表格使用中有泄漏問題
7、二分法刪除表格的業(yè)務代碼,最后發(fā)現(xiàn)表格的slot寫法中有多處泄露(代碼可參考后面的截圖)。一個是,div的樣式類是動態(tài)的,它使用了表格數(shù)據(jù)來判斷是否有該樣式類;另一個是,表格的一列單元格的內(nèi)容是字典,需要根據(jù)字典的code查找出對應的label。得出結(jié)論:打開對話框出現(xiàn)內(nèi)存泄露只是表象,在這個問題中,任何添加dom的操作,都會導致dom只增不減,因此切換多個表格也會出現(xiàn)同樣的泄露問題,表格出了問題,不是對話框出了問題;更重要的一點是,table組件的slot中,盡量不要使用渲染數(shù)據(jù)來做判斷、也不要作為內(nèi)聯(lián)函數(shù)的參數(shù)

錯誤一:使用表格數(shù)據(jù)動態(tài)添加樣式類
錯誤二:表格單元格渲染依賴內(nèi)聯(lián)函數(shù)

8、錯誤修復
問題一如圖,數(shù)據(jù)中提前計算好樣式類,table組件的slot中直接使用,不再計算


錯誤一修改:數(shù)據(jù)提前計算樣式并保存在數(shù)據(jù)中
錯誤一修改:dom中使用數(shù)據(jù)中提前算好的樣式類

問題二如圖:表格渲染時,內(nèi)聯(lián)函數(shù)中不要把渲染的行數(shù)據(jù)作為參數(shù)傳遞,只傳遞行數(shù)據(jù)的下表index,然后行數(shù)據(jù)可以通過tableData[index]方式獲取到


錯誤二修復:內(nèi)聯(lián)函數(shù)參數(shù)用scope.$index替換原來的scope.row
錯誤二修復:內(nèi)聯(lián)函數(shù)中通過index獲取行數(shù)據(jù)rowItem

注意:內(nèi)聯(lián)函數(shù)中,當index為-1時,或者tableData沒有數(shù)據(jù)時,一定要返回,否則依然會泄露,這個點比較隱秘,很難發(fā)現(xiàn)

四、案例三:給子組件傳參引發(fā)的內(nèi)存泄露

如下圖,左側(cè)是有問題的代碼,右側(cè)是修改后的。問題仍然是將表格的某一行數(shù)據(jù)作為參數(shù)傳遞給子組件。無論子組件是否修改了改參數(shù),都會導致dom不釋放。右側(cè)是修改后的代碼,修改思路依然是,不傳遞表格的行數(shù)據(jù),可以傳遞表格全部數(shù)據(jù)以及當前行的下標,子組件根據(jù)這兩個參數(shù)就能獲取到當前行的數(shù)據(jù)。


image.png

五、案例四:樹組件的子節(jié)點數(shù)據(jù)懶加載回來后,將數(shù)據(jù)綁定在當前點擊的父節(jié)點node上,此處直接修改treeData的一個節(jié)點數(shù)據(jù),導致舊的表格數(shù)據(jù)沒有釋放,出現(xiàn)內(nèi)存增長從而泄露。

image.png

六、工具使用:chrome性能監(jiān)控

1、兩種方式,第一個比較難用,第二個比較常用

1、chrom的開發(fā)者模式中,memory頁簽下,可以抓取兩次快照,對比內(nèi)存泄露的大小、泄露的對象、泄露的文件和代碼。
【點評】親測不好用,因為vue3組件嵌套過深,依賴的框架和第三方較多,對比出來的維度和條目有十幾萬條,并且結(jié)果中將插件和自己編譯后的代碼混合在一起,很難找到自己寫的文件的泄露點,總體來說,就像大海撈針

2、chrom的devTools中,右上角的三個點標志中,找到more tools中的performance monitor,然后結(jié)合memory頁簽下的回收內(nèi)存按鈕,就可以監(jiān)控js內(nèi)存、dom大小和事件監(jiān)聽。js內(nèi)存和dom持續(xù)上漲,一定有泄露,最實用也是最靠譜的辦法是,二分法刪除代碼,不斷縮小范圍,最后鎖定問題。
【點評】性能監(jiān)控工具,就是一個指南針,給出一個方向和泄露體量的指標值。要解決具體問題,還是得逐行查看自己的代碼。這個過程一定會非常耗時,做好準備,踏實使用二分法對代碼地毯式排查,沒有捷徑。因為每一個問題都有可能與之前的不一樣,之前積累的經(jīng)驗在關(guān)鍵的時候有用。但是,達到關(guān)鍵點之前,大量的排查工作,將問題鎖定到很小的范圍之內(nèi),是必不可少的,少了排查,關(guān)鍵點不可能到來

2、方式一使用說明

兩次快照對比時,字段的意義如下:

  1. alloc.size (Allocated Size)
    定義:在兩次快照之間新分配的內(nèi)存總量

計算方式:sum(所有新創(chuàng)建對象的內(nèi)存大小)

正常情況:在操作過程中會有合理的新內(nèi)存分配

異常信號:持續(xù)高 alloc.size 且不被釋放可能表示泄漏

  1. freed size (Freed Size)
    定義:在兩次快照之間被垃圾回收的內(nèi)存總量

計算方式:sum(所有被銷毀對象的內(nèi)存大小)

健康模式:應該與 alloc.size 保持相對平衡

危險信號:長期低 freed size 表明對象未被正確釋放

  1. size delta (Size Delta)
    定義:內(nèi)存凈變化量

計算公式:size delta = alloc.size - freed.size

解讀:

0:內(nèi)存凈增長(潛在泄漏)

≈0:內(nèi)存平衡(健康狀態(tài))

<0:內(nèi)存釋放(可能是緩存清理)

內(nèi)存泄漏判斷標準-確定泄漏的強信號:

連續(xù)多次快照對比都顯示 size delta > 0

freed size ≈ 0 而 alloc.size 持續(xù)增長

特定對象類型的 delta 持續(xù)為正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容