ECharts 内存管理与dispose完全指南
文档类型: 深度技术文档
难度等级: ⭐⭐⭐
源码版本: ECharts 5.x
本文行数: 约500行
📋 目录
🎯 内存泄漏原因分析
常见内存泄漏场景
内存泄漏的影响
| 指标 | 正常情况 | 泄漏后(1小时) | 影响 |
|---|---|---|---|
| 内存占用 | 50MB | 500MB+ | 10x增长 ❌ |
| 页面卡顿 | 流畅 | 严重 | FPS < 10 |
| 浏览器崩溃 | 无 | 频繁 | 无法使用 |
⚙️ dispose方法详解
基础用法
typescript
// 创建图表
const chart = echarts.init(container);
chart.setOption(option);
// ✅ 正确: 销毁图表释放资源
chart.dispose();
// ❌ 错误: 仅隐藏但不释放内存
container.style.display = 'none';1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
dispose做了什么?
typescript
// ECharts内部伪代码
class ECharts {
dispose() {
// 1. 销毁ZRender渲染器
this.zr.dispose();
// 2. 清除Canvas/SVG元素
this.canvas = null;
// 3. 移除所有事件监听器
this.removeAllListeners();
// 4. 清空内部数据引用
this.option = null;
this.model = null;
// 5. 取消动画帧
cancelAnimationFrame(this.animationFrame);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
🚀 生命周期管理最佳实践
模式1: try-finally确保释放
typescript
let chart: echarts.ECharts | null = null;
try {
chart = echarts.init(container);
chart.setOption(option);
// 执行业务逻辑
doSomething(chart);
} catch (error) {
console.error('图表操作失败:', error);
} finally {
// 无论如何都会执行
if (chart) {
chart.dispose();
chart = null;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
模式2: 资源管理器
typescript
class ChartResourceManager {
private charts: Map<string, echarts.ECharts> = new Map();
/**
* 创建并注册图表
*/
createChart(id: string, container: HTMLElement): echarts.ECharts {
const chart = echarts.init(container);
this.charts.set(id, chart);
return chart;
}
/**
* 销毁并注销图表
*/
disposeChart(id: string) {
const chart = this.charts.get(id);
if (chart) {
chart.dispose();
this.charts.delete(id);
console.log(`图表 ${id} 已销毁`);
}
}
/**
* 销毁所有图表
*/
disposeAll() {
this.charts.forEach((chart, id) => {
chart.dispose();
console.log(`图表 ${id} 已销毁`);
});
this.charts.clear();
}
/**
* 获取图表数量
*/
getChartCount(): number {
return this.charts.size;
}
}
// 使用
const resourceManager = new ChartResourceManager();
// 创建
const chart1 = resourceManager.createChart('chart1', container1);
const chart2 = resourceManager.createChart('chart2', container2);
console.log(`活跃图表: ${resourceManager.getChartCount()}`); // 2
// 统一销毁
resourceManager.disposeAll();
console.log(`活跃图表: ${resourceManager.getChartCount()}`); // 01
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
模式3: 自动清理包装器
typescript
class AutoCleanupChart {
private chart: echarts.ECharts | null = null;
private timers: number[] = [];
private listeners: Array<{ event: string; handler: Function }> = [];
constructor(private container: HTMLElement) {}
/**
* 初始化图表
*/
init(option: any) {
this.chart = echarts.init(this.container);
this.chart.setOption(option);
return this;
}
/**
* 添加定时任务 (自动跟踪)
*/
addInterval(callback: () => void, delay: number): number {
const timer = window.setInterval(callback, delay);
this.timers.push(timer);
return timer;
}
/**
* 添加事件监听 (自动跟踪)
*/
on(event: string, handler: Function) {
if (this.chart) {
this.chart.on(event, handler);
this.listeners.push({ event, handler });
}
return this;
}
/**
* 销毁 - 自动清理所有资源
*/
destroy() {
// 1. 清除所有定时器
this.timers.forEach(timer => clearInterval(timer));
this.timers = [];
// 2. 移除所有事件监听
if (this.chart) {
this.listeners.forEach(({ event, handler }) => {
this.chart!.off(event, handler);
});
this.listeners = [];
// 3. 销毁图表
this.chart.dispose();
this.chart = null;
}
console.log('所有资源已清理');
}
}
// 使用
const autoChart = new AutoCleanupChart(container);
autoChart
.init(option)
.on('click', handleClick)
.addInterval(() => {
// 实时更新
autoChart.chart?.setOption(newData);
}, 1000);
// 页面卸载时
autoChart.destroy(); // 一键清理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
🖼️ SPA框架中的内存管理
React - useEffect清理
typescript
import React, { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
export function ChartComponent({ option }: { option: any }) {
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
useEffect(() => {
if (!chartRef.current) return;
// 初始化
chartInstance.current = echarts.init(chartRef.current);
chartInstance.current.setOption(option);
// ✅ 清理函数 - 组件卸载时自动调用
return () => {
if (chartInstance.current) {
chartInstance.current.dispose();
chartInstance.current = null;
console.log('图表已销毁');
}
};
}, []);
// 更新option
useEffect(() => {
if (chartInstance.current && option) {
chartInstance.current.setOption(option);
}
}, [option]);
return <div ref={chartRef} style={{ width: '100%', height: '400px' }} />;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Vue 3 - onUnmounted清理
vue
<template>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
const chartRef = ref<HTMLElement | null>(null);
const chartInstance = ref<echarts.ECharts | null>(null);
const props = defineProps<{
option: any
}>();
// 挂载时初始化
onMounted(() => {
if (chartRef.value) {
chartInstance.value = echarts.init(chartRef.value);
chartInstance.value.setOption(props.option);
}
});
// ✅ 卸载时销毁
onUnmounted(() => {
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = null;
console.log('图表已销毁');
}
});
// 监听option变化
watch(() => props.option, (newOption) => {
if (chartInstance.value && newOption) {
chartInstance.value.setOption(newOption);
}
}, { deep: true });
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Vue Router - 路由切换时清理
typescript
import { onBeforeRouteLeave } from 'vue-router';
export default {
setup() {
const chartInstance = ref<echarts.ECharts | null>(null);
onMounted(() => {
chartInstance.value = echarts.init(container);
});
// ✅ 离开路由前销毁
onBeforeRouteLeave((to, from, next) => {
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = null;
}
next();
});
return {};
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Angular - ngOnDestroy清理
typescript
import { Component, OnInit, OnDestroy, ElementRef } from '@angular/core';
import * as echarts from 'echarts';
@Component({
selector: 'app-chart',
template: '<div #chartContainer style="width:100%;height:400px"></div>'
})
export class ChartComponent implements OnInit, OnDestroy {
private chart: echarts.ECharts | null = null;
@ViewChild('chartContainer') container!: ElementRef;
ngOnInit() {
this.chart = echarts.init(this.container.nativeElement);
this.chart.setOption(this.option);
}
// ✅ 组件销毁时调用
ngOnDestroy() {
if (this.chart) {
this.chart.dispose();
this.chart = null;
console.log('图表已销毁');
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
🔍 内存泄漏检测工具
工具1: Chrome DevTools Memory面板
typescript
// 1. 打开Chrome DevTools → Memory
// 2. 点击 "Take heap snapshot"
// 3. 执行操作 (如切换路由)
// 4. 再次拍摄快照
// 5. 对比两个快照,查找未释放的ECharts实例
// 在Console中检查
console.log(echarts.getInstanceByDom(container));
// 如果返回null说明已正确销毁1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
工具2: 自定义内存监控
typescript
class MemoryMonitor {
private baselineMemory: number = 0;
/**
* 记录基线内存
*/
recordBaseline() {
if (performance.memory) {
this.baselineMemory = performance.memory.usedJSHeapSize;
console.log(`基线内存: ${this.formatBytes(this.baselineMemory)}`);
}
}
/**
* 检查内存泄漏
*/
checkLeak() {
if (performance.memory) {
const currentMemory = performance.memory.usedJSHeapSize;
const diff = currentMemory - this.baselineMemory;
console.log(`
当前内存: ${this.formatBytes(currentMemory)}
差异: ${this.formatBytes(diff)}
`);
if (diff > 10 * 1024 * 1024) { // 超过10MB
console.warn('⚠️ 可能存在内存泄漏');
}
} else {
console.warn('当前浏览器不支持内存监控');
}
}
private formatBytes(bytes: number): string {
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
}
}
// 使用
const monitor = new MemoryMonitor();
// 页面加载时记录基线
monitor.recordBaseline();
// 定期检测
setInterval(() => {
monitor.checkLeak();
}, 60000); // 每分钟检测一次1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
工具3: 自动化测试
typescript
describe('Chart Memory Leak Test', () => {
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
// 确保每次测试后都清理
const chart = echarts.getInstanceByDom(container);
if (chart) {
chart.dispose();
}
document.body.removeChild(container);
});
it('should not leak memory after dispose', () => {
const chart = echarts.init(container);
chart.setOption({ /* ... */ });
// 验证图表存在
expect(echarts.getInstanceByDom(container)).toBeTruthy();
// 销毁
chart.dispose();
// 验证图表已销毁
expect(echarts.getInstanceByDom(container)).toBeNull();
});
it('should release all event listeners', () => {
const chart = echarts.init(container);
const handler = jest.fn();
chart.on('click', handler);
chart.dispose();
// 触发事件应该无效
chart.getZr().handler.dispatch('click', {});
expect(handler).not.toHaveBeenCalled();
});
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
🎯 终极检查清单
销毁图表时必须做的事
- [ ] 调用
chart.dispose() - [ ] 将引用设为
null - [ ] 清除所有定时器 (
clearInterval,clearTimeout) - [ ] 移除所有事件监听器 (
chart.off()) - [ ] 取消动画帧 (
cancelAnimationFrame)
框架特定清理
- [ ] React: useEffect返回清理函数
- [ ] Vue 3: onUnmounted中调用dispose
- [ ] Angular: ngOnDestroy中调用dispose
- [ ] 路由切换: onBeforeRouteLeave中清理
验证是否泄漏
- [ ] 使用Chrome Memory面板对比快照
- [ ] 检查
echarts.getInstanceByDom(container)返回null - [ ] 长时间运行后内存稳定
🔗 相关资源
上一篇: 关闭非必要特效
✅ 性能优化模块完成!
