Reader

前端佬们!塌房了!用过Element-Plus的进来~

| 掘金本周最热 | Default

进来着急的前端佬,我直接抛出结论吧!

Element-plus的组件,经过测验,如下组件存在内存泄漏。如下:

  • 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 Version: 3.5.13
Element Plus Version: 2.9.7
Browser / OS: window 10 / Edge 134.0.3124.85 (正式版本) (64 位)
Build Tool: Webpack

不排查ElementUI也存在这个问题。

好了。接下来细细聊。

pcJpq404.gif

前因

为什么检测到这种问题?主要因为一个项目引用了Element-plus。然后,你懂的,买的人永远都会想要最好的,然后买的人就这么一顿狂点Web页面,看见内存占用飙到老高。

于是...前端佬都懂的,来活了。

7166eec470f04755a2f52fe819a62493.gif

排查

一开始我是不敢怀疑这种高star开源组件的。总以为自己是写的代码有问题。

详细代码就不贴了,主要用ElDialog组件,封装成一个命令式的Dialog组件,避免频繁的使用v-modal参数。

然后,就直接怀疑上这个组件了。

经过测试,果不其然,从关闭到销毁,会导致内存猛增,因为Dialog中有各种表单组件,一打开就创建了一大堆的Element元素。

image.png

精确定位,使用了FinalizationRegistry类追踪创建的Dialog实体,代码如下:

const finalizerRegistry = new FinalizationRegistry((heldValue) => {
  console.log('Finalizing instance: ',heldValue);
});


// 在创建处监听
const heldValue = Symbol(`DialogCommandComponent_${Date.now()}`);
finalizerRegistry.register(this, heldValue);
console.log(`Constructed instance:`,heldValue);

发现一直没有Constructed instance销毁的信息输出。

随后,使用了Edge浏览器中的分离元素来打快照,步骤如下图。

image.png

经过反复的操作,然后点击主动垃圾回收,然后发现el-dialog的元素都会增加,基本确认无疑了。

但还是怀疑,会不会是Dialog中,引用的问题,导致元素一直没能销毁?所以,使用了纯纯的el-dialog来校验,同样的操作,既然如故。

a4.jpg

然后的然后,我使用了如下的代码,去校验其它组件是否存在同样的问题。代码如下:

<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>

如上代码,进入页面后,点击click,然后关闭所有的弹窗。然后再次点击reset按钮,然后再次点击click,关闭所有弹窗。如此可以多操作几次。

就发现了开头的组件,都存在内存泄漏问题。

未能解决

有问题,当然首先看看别人有没有出现过。各种搜索就不说了,大掘金也搜过,在Element-plus的github仓里的Issues中找过,发现的办法基本无用。

以下是自己思考的几条路子:

  1. 有泄漏的,都手搓一个?
  2. Eldialog全局只用一到两个?
  3. 将所有路由,都打成一个单页面(html)。
  4. 改源码....

结尾

还是在这里,求助大佬,看以上思路是否有错,然后跪求orz解决办法。

自己后续如果解决对应一些问题,会即时和大家分享。