ECharts 忘记dispose内存泄漏完全指南
文档类型: 反模式警示
难度等级: ⭐⭐⭐
源码版本: ECharts 5.x
本文行数: 约480行
📋 目录
🎯 问题严重性
内存泄漏后果
忘记调用dispose()会导致:
- ❌ Canvas/SVG元素残留
- ❌ 事件监听器未移除
- ❌ ZRender实例泄露
- ❌ 内存持续增长
- ❌ 页面卡顿甚至崩溃
实际案例:
单页应用运行2小时后:
- 内存占用: 50MB → 800MB (16倍增长!)
- FPS: 60 → 15 (严重卡顿)
- 最终浏览器崩溃1
2
3
4
2
3
4
❌ 常见场景
场景1: SPA路由切换
typescript
// Vue Router示例
const routes = [
{
path: '/chart',
component: ChartPage // 包含ECharts图表
}
];
// ❌ 错误: 离开页面时未销毁
router.push('/other-page');
// chart实例仍在内存中!1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
场景2: React组件卸载
typescript
function ChartComponent() {
useEffect(() => {
const chart = echarts.init(container);
chart.setOption(option);
// ❌ 错误: 缺少清理函数
// return () => chart.dispose();
}, []);
return <div ref={container} />;
}
// 组件卸载后,chart实例泄露!1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
场景3: 动态创建/销毁容器
typescript
// ❌ 错误示范
function showChart() {
const container = document.createElement('div');
document.body.appendChild(container);
const chart = echarts.init(container);
chart.setOption(option);
}
function hideChart() {
// 仅移除DOM,未销毁chart
document.body.removeChild(container); // ❌ 内存泄漏!
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
场景4: Tab切换
typescript
// ❌ 错误: 切换Tab时未销毁
tabs.forEach(tab => {
tab.addEventListener('click', () => {
if (tab.id === 'chart-tab') {
const chart = echarts.init(container);
chart.setOption(option);
}
// 切换到其他Tab时,chart未销毁!
});
});1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
✅ 解决方案
方案1: Vue 3 - onUnmounted
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import * as echarts from 'echarts';
const chartInstance = ref<echarts.ECharts | null>(null);
onMounted(() => {
chartInstance.value = echarts.init(document.getElementById('chart')!);
chartInstance.value.setOption(option);
});
// ✅ 正确: 组件卸载时销毁
onUnmounted(() => {
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = null;
console.log('图表已销毁,释放内存');
}
});
</script>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
方案2: React - useEffect清理
typescript
import { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
function ChartComponent() {
const chartRef = useRef<echarts.ECharts | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
// 初始化
chartRef.current = echarts.init(containerRef.current);
chartRef.current.setOption(option);
// ✅ 正确: 返回清理函数
return () => {
if (chartRef.current) {
chartRef.current.dispose();
chartRef.current = null;
console.log('图表已销毁');
}
};
}, []);
return <div ref={containerRef} style={{ width: '100%', height: 400 }} />;
}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
方案3: 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(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
方案4: 资源管理器模式
typescript
class ChartResourceManager {
private charts: Map<string, echarts.ECharts> = new Map();
/**
* 注册图表
*/
register(id: string, chart: echarts.ECharts) {
this.charts.set(id, chart);
}
/**
* 销毁指定图表
*/
dispose(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();
}
/**
* 获取活跃图表数量
*/
getCount(): number {
return this.charts.size;
}
}
// 全局单例
export const chartManager = new ChartResourceManager();
// 使用
const chart = echarts.init(container);
chartManager.register('my-chart', chart);
// 页面卸载时
window.addEventListener('beforeunload', () => {
chartManager.disposeAll(); // ✅ 统一清理
});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
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
🔍 检测工具
工具1: Chrome DevTools
typescript
// 1. 打开Chrome DevTools → Memory
// 2. 拍摄堆快照 (Heap Snapshot)
// 3. 执行操作 (如切换路由)
// 4. 再次拍摄快照
// 5. 对比两个快照
// 在Console中检查
console.log('ECharts实例数:', echarts.getInstanceCount());
// 如果数字持续增长,说明有内存泄漏1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
工具2: 自定义监控
typescript
class MemoryLeakDetector {
private baselineCount: number = 0;
constructor() {
this.baselineCount = echarts.getInstanceCount();
console.log(`基线ECharts实例数: ${this.baselineCount}`);
}
/**
* 检查是否有泄漏
*/
check() {
const currentCount = echarts.getInstanceCount();
const diff = currentCount - this.baselineCount;
console.log(`
当前实例数: ${currentCount}
差异: ${diff}
`);
if (diff > 5) {
console.warn('⚠️ 可能存在内存泄漏!');
console.warn('未销毁的实例:', diff);
}
return diff;
}
/**
* 定期检测
*/
startMonitoring(interval: number = 60000) {
setInterval(() => {
this.check();
}, interval);
}
}
// 使用
const detector = new MemoryLeakDetector();
detector.startMonitoring(30000); // 每30秒检测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
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
工具3: 自动化测试
typescript
describe('Memory Leak Test', () => {
it('should dispose chart on unmount', () => {
const initialCount = echarts.getInstanceCount();
// 挂载组件
const { unmount } = render(<ChartComponent />);
expect(echarts.getInstanceCount()).toBe(initialCount + 1);
// 卸载组件
unmount();
// ✅ 验证实例已销毁
expect(echarts.getInstanceCount()).toBe(initialCount);
});
it('should release all resources', () => {
const container = document.createElement('div');
const chart = echarts.init(container);
// 添加事件监听
chart.on('click', () => {});
// 销毁
chart.dispose();
// 验证无法再访问
expect(() => chart.setOption({})).toThrow();
});
});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
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
💻 实战案例
案例: Dashboard页面内存管理
typescript
class DashboardManager {
private charts: echarts.ECharts[] = [];
init() {
// 创建多个图表
const chart1 = echarts.init(document.getElementById('chart1')!);
const chart2 = echarts.init(document.getElementById('chart2')!);
const chart3 = echarts.init(document.getElementById('chart3')!);
chart1.setOption(option1);
chart2.setOption(option2);
chart3.setOption(option3);
this.charts = [chart1, chart2, chart3];
console.log(`创建了 ${this.charts.length} 个图表`);
}
/**
* ✅ 正确: 统一销毁所有图表
*/
destroy() {
this.charts.forEach((chart, index) => {
chart.dispose();
console.log(`图表 ${index + 1} 已销毁`);
});
this.charts = [];
console.log('所有图表已清理');
}
/**
* 获取内存使用情况
*/
getMemoryInfo() {
return {
chartCount: this.charts.length,
instanceCount: echarts.getInstanceCount()
};
}
}
// 使用
const dashboard = new DashboardManager();
// 进入页面
dashboard.init();
// 离开页面
window.addEventListener('beforeunload', () => {
dashboard.destroy(); // ✅ 确保清理
});
// 或在Vue/React中
onUnmounted(() => {
dashboard.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
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
🎯 最佳实践总结
✅ DO - 推荐做法
始终在生命周期钩子中销毁
typescript// Vue onUnmounted(() => chart.dispose()) // React return () => chart.dispose() // Angular ngOnDestroy() { chart.dispose() }1
2
3
4
5
6
7
8使用资源管理器统一管理
typescriptchartManager.register(id, chart); chartManager.disposeAll();1
2定期检查内存泄漏
typescriptconsole.log(echarts.getInstanceCount());1
❌ DON'T - 避免做法
不要仅移除DOM而不销毁
typescript// ❌ 错误 container.remove(); // ✅ 正确 chart.dispose(); container.remove();1
2
3
4
5
6不要在循环中创建而不销毁
typescript// ❌ 错误 for (let i = 0; i < 100; i++) { const chart = echarts.init(container); } // ✅ 正确 const chart = echarts.init(container); // ... 使用 chart.dispose();1
2
3
4
5
6
7
8
9
🔗 相关资源
下一篇: 大数据开-tooltip
