ElementPlus 被發(fā)現(xiàn)重大 BUG!可能會(huì)讓頁(yè)面崩掉!
前端開(kāi)發(fā)者們注意啦,今天給大家?guī)?lái)一個(gè)關(guān)于Element-plus組件的內(nèi)存泄漏問(wèn)題,經(jīng)過(guò)測(cè)試,以下組件存在內(nèi)存泄漏的現(xiàn)象:
- el-carousel
- el-select + el-options
- el-descriptions
- el-tag
- el-dialog
- el-notification
- el-loading
- el-result
- el-message
- el-button
- el-tabs
- el-menu
- el-popper
以下是我在 Vue 3.5.13
和 Element Plus 2.9.7
版本下的排查環(huán)境:
- Vue版本:3.5.13
- Element Plus版本:2.9.7
- 操作系統(tǒng):Windows 10
- 瀏覽器:Edge 134.0.3124.85 (正式版)
- 構(gòu)建工具:Webpack
排查過(guò)程
一開(kāi)始,我也沒(méi)有懷疑這種高 Star 的開(kāi)源組件,覺(jué)得肯定是我寫(xiě)的代碼有問(wèn)題。為了追查到底,我主要用了 ElDialog
組件,封裝成了一個(gè)命令式的 Dialog
組件,避免頻繁使用 v-model
參數(shù)。然后,我就把懷疑的目光投向了 ElDialog
。
結(jié)果,經(jīng)過(guò)測(cè)試,果然,Dialog
組件在關(guān)閉和銷(xiāo)毀時(shí),會(huì)導(dǎo)致內(nèi)存使用飆升。特別是當(dāng) Dialog
中包含各種表單組件時(shí),一旦打開(kāi),就會(huì)創(chuàng)建大量的 Element 元素,導(dǎo)致內(nèi)存泄漏。為了解決這個(gè)問(wèn)題,我用了 FinalizationRegistry
類(lèi)追蹤 Dialog 組件實(shí)例的銷(xiāo)毀,代碼如下:
const finalizerRegistry = new FinalizationRegistry((heldValue) => {
console.log('Finalizing instance: ',heldValue);
});
// 在創(chuàng)建處監(jiān)聽(tīng)
const heldValue = Symbol(`DialogCommandComponent_${Date.now()}`);
finalizerRegistry.register(this, heldValue);
console.log(`Constructed instance:`,heldValue);
通過(guò)測(cè)試,發(fā)現(xiàn)Dialog組件的銷(xiāo)毀過(guò)程沒(méi)有產(chǎn)生銷(xiāo)毀信息,這意味著它沒(méi)有正確地釋放資源。于是,我決定進(jìn)一步分析,是否是Dialog組件內(nèi)部的引用問(wèn)題導(dǎo)致元素未能銷(xiāo)毀。我嘗試用純el-dialog組件進(jìn)行測(cè)試,結(jié)果同樣發(fā)現(xiàn)內(nèi)存泄漏。
圖片
擴(kuò)展測(cè)試
為了進(jìn)一步驗(yàn)證其他組件是否也存在類(lèi)似問(wèn)題,我編寫(xiě)了一段代碼,檢查了多個(gè) Element-plus
組件的內(nèi)存占用情況。以下是相關(guān)代碼:
<template>
<div>
<el-button @click="fn2">Reset</el-button>
</div>
<el-dialog v-model="model" destroy-on-close @closed="fn1" append-to-body v-if="destroyDialogModelValue"></el-dialog>
<el-button @click="fn0" v-if="!button" primse>Click</el-button>
<div class="weak" v-if="!button">xxx</div>
<el-input v-if="!button" />
<el-border v-if="!button" />
<el-select v-if="!button">
<el-option>1111</el-option>
</el-select>
<el-switch v-if="!button" />
<el-radio v-if="!button" />
<el-rate v-if="!button" />
<el-slider v-if="!button" />
<el-time-picker v-if="!button" />
<el-time-select v-if="!button" />
<el-transfer v-if="!button" />
<el-tree-select v-if="!button" />
<el-calendar v-if="!button" />
<el-card v-if="!button" />
<el-carousel height="150px" v-if="!button">
<el-carousel-item v-for="item in 4" :key="item">
<h3 class="small justify-center" text="2xl">{{ item }}</h3>
</el-carousel-item>
</el-carousel>
<el-descriptions title="User Info" v-if="!button">
<el-descriptions-item label="Username">kooriookami</el-descriptions-item>
</el-descriptions>
<el-table style="width: 100%" v-if="!button">
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
</el-table>
<el-avatar v-if="!button" />
<el-pagination layout="prev, pager, next" :total="50" v-if="!button" />
<el-progress :percentage="50" v-if="!button" />
<el-result icon="success" title="Success Tip" sub-title="Please follow the instructions" v-if="!button">
<template #extra>
<el-button type="primary">Back</el-button>
</template>
</el-result>
<el-skeleton v-if="!button" />
<el-tag v-if="!button" />
<el-timeline v-if="!button" />
<el-tree v-if="!button" />
<el-avatar v-if="!button" />
<el-segmented size="large" v-if="!button" />
<el-dropdown v-if="!button">
<span class="el-dropdown-link">
Dropdown List
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Action 1</el-dropdown-item>
<el-dropdown-item>Action 2</el-dropdown-item>
<el-dropdown-item>Action 3</el-dropdown-item>
<el-dropdown-item disabled>Action 4</el-dropdown-item>
<el-dropdown-item divided>Action 5</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-menu class="el-menu-demo" mode="horizontal" v-if="!button">
<el-menu-item index="1">Processing Center</el-menu-item>
<el-sub-menu index="2">
<template #title>Workspace</template>
<el-menu-item index="2-1">item one</el-menu-item>
<el-menu-item index="2-2">item two</el-menu-item>
<el-menu-item index="2-3">item three</el-menu-item>
<el-sub-menu index="2-4">
<template #title>item four</template>
<el-menu-item index="2-4-1">item one</el-menu-item>
<el-menu-item index="2-4-2">item two</el-menu-item>
<el-menu-item index="2-4-3">item three</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="3" disabled>Info</el-menu-item>
<el-menu-item index="4">Orders</el-menu-item>
</el-menu>
<el-steps style="max-width: 600px" active="0" finish-status="success" v-if="!button">
<el-step title="Step 1" />
<el-step title="Step 2" />
<el-step title="Step 3" />
</el-steps>
<el-tabs class="demo-tabs" v-if="!button">
<el-tab-pane label="User" name="first">User</el-tab-pane>
<el-tab-pane label="Config" name="second">Config</el-tab-pane>
<el-tab-pane label="Role" name="third">Role</el-tab-pane>
<el-tab-pane label="Task" name="fourth">Task</el-tab-pane>
</el-tabs>
<el-alert title="Success alert" type="success" v-if="!button" />
<el-drawer title="I am the title" v-if="!button">
<span>Hi, there!</span>
</el-drawer>
<div v-loading="model" v-if="!button"></div>
<el-popconfirm confirm-button-text="Yes" cancel-button-text="No" icon-color="#626AEF"
title="Are you sure to delete this?" v-if="!button">
<template #reference>
<el-button>Delete</el-button>
</template>
</el-popconfirm>
<el-popover class="box-item" title="Title" content="Top Center prompts info" placement="top" v-if="!button">
<template #reference>
<div>top</div>
</template>
</el-popover>
<el-tooltip class="box-item" effect="dark" content="Top Left prompts info" placement="top-start" v-if="!button">
<div>top-start</div>
</el-tooltip>
</template>
<script setup>
import { ref } from"vue";
import { ElMessage, ElMessageBox, ElNotification } from"element-plus";
const model = ref(false);
const destroyDialogModelValue = ref(false);
const button = ref(false);
function fn0() {
model.value = true;
destroyDialogModelValue.value = true;
ElMessage("This is a message.");
ElMessageBox.alert("This is a message", "Title");
ElNotification({
title: "Title",
message: "This is a reminder",
});
}
function fn1() {
console.log("closed");
destroyDialogModelValue.value = false;
button.value = true;
}
function reset() {
model.value = false
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
每次點(diǎn)擊“Click”按鈕后,我關(guān)閉所有彈窗,再點(diǎn)擊“Reset”按鈕,然后重復(fù)上述操作,發(fā)現(xiàn)內(nèi)存占用一直在上漲。經(jīng)過(guò)反復(fù)操作,最終確定了Element-plus的多個(gè)組件存在內(nèi)存泄漏問(wèn)題。
未能解決
面對(duì)這個(gè)問(wèn)題,我也查找了大量的資源,包括 Element-plus
的GitHub issues和相關(guān)論壇,但現(xiàn)有的解決方法基本無(wú)效。經(jīng)過(guò)思考,以下是我想到的幾種可能的解決方案:
- 是否可以為有泄漏的組件手動(dòng)實(shí)現(xiàn)銷(xiāo)毀機(jī)制?
- ElDialog是否只在全局使用一兩個(gè)實(shí)例?
- 是否能將所有路由都打成單頁(yè)面應(yīng)用(SPA)?
- 是否需要修改源碼?
目前,這些方案都沒(méi)有給我?guī)?lái)太大突破,但仍在繼續(xù)研究中。