ECharts 代码分割优化
📋 概述
ECharts作为功能丰富的图表库,完整版本体积较大。通过代码分割(Code Splitting)和Tree Shaking技术,可以显著减少最终打包体积:
- ✅ 按需引入:只打包使用的图表类型和组件
- ✅ Tree Shaking:移除未使用的代码
- ✅ Bundle分析:可视化查看包体积组成
- ✅ 运行时优化:减少内存占用和初始化时间
本文档详细介绍ECharts的代码分割策略和优化技巧。
🎯 核心概念
代码分割层次
┌─────────────────────────────────────┐
│ Level 1: 库级别分割 │
│ ├─ echarts/core │
│ ├─ echarts/charts/* │
│ └─ echarts/components/* │
├─────────────────────────────────────┤
│ Level 2: 业务模块分割 │
│ ├─ charts/line-chart │
│ ├─ charts/bar-chart │
│ └─ charts/pie-chart │
├─────────────────────────────────────┤
│ Level 3: 数据分割 │
│ ├─ data preprocessing │
│ └─ data transformation │
└─────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Tree Shaking原理
typescript
// ❌ 全量引入 - 无法Tree Shaking
import * as echarts from 'echarts';
// ✅ 按需引入 - 支持Tree Shaking
import * as echarts from 'echarts/core';
import { LineChart } from 'echarts/charts';
import { GridComponent } from 'echarts/components';1
2
3
4
5
6
7
2
3
4
5
6
7
🔧 优化实现
1. 模块化引入
Vite + Vue3配置
typescript
// utils/chart.ts
import * as echarts from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
// 图表类型
import {
LineChart,
BarChart,
PieChart
} from 'echarts/charts';
// 组件
import {
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent
} from 'echarts/components';
// 注册需要的模块
echarts.use([
CanvasRenderer,
LineChart,
BarChart,
PieChart,
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent
]);
export default echarts;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
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
Webpack + React配置
typescript
// chart-factory.ts
import * as echarts from 'echarts/core';
import type { ComposeOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
/**
* 创建特定类型的图表工厂
*/
export function createLineChart() {
return async () => {
const { LineChart } = await import('echarts/charts');
const { GridComponent, TooltipComponent } = await import('echarts/components');
echarts.use([LineChart, GridComponent, TooltipComponent, CanvasRenderer]);
return (container: HTMLElement, option: any) => {
return echarts.init(container);
};
};
}
export function createBarChart() {
return async () => {
const { BarChart } = await import('echarts/charts');
const { GridComponent, TooltipComponent, LegendComponent } = await import('echarts/components');
echarts.use([BarChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);
return (container: HTMLElement, option: any) => {
return echarts.init(container);
};
};
}
// 使用示例
const lineChartFactory = await createLineChart();
const initLineChart = await lineChartFactory();
const chart = initLineChart(container);
chart.setOption(lineOption);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
2. Bundle分析
Webpack Bundle Analyzer
javascript
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
// ...其他配置
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'server', // 启动本地服务器显示报告
analyzerHost: '127.0.0.1',
analyzerPort: 8888,
reportFilename: 'report.html',
defaultSizes: 'parsed', // parsed | stat | gzip
openAnalyzer: true, // 自动打开浏览器
generateStatsFile: false,
statsFilename: 'stats.json',
logLevel: 'info'
})
]
};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
Rollup Visualizer
javascript
// rollup.config.js
import visualizer from 'rollup-plugin-visualizer';
export default {
// ...其他配置
plugins: [
visualizer({
filename: 'bundle-stats.html',
open: true,
gzipSize: true,
brotliSize: true
})
]
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Vite Bundle分析
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true,
template: 'treemap' // treemap | sunburst | network
})
],
build: {
rollupOptions: {
output: {
manualChunks: {
'echarts-core': ['echarts/core'],
'echarts-charts': ['echarts/charts'],
'echarts-components': ['echarts/components']
}
}
}
}
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
3. 手动Chunk分割
Webpack魔法注释
typescript
/**
* 按路由分割代码
*/
const Dashboard = lazy(() =>
import(/* webpackChunkName: "dashboard" */ './views/Dashboard')
);
const Reports = lazy(() =>
import(/* webpackChunkName: "reports" */ './views/Reports')
);
/**
* 按图表类型分割
*/
async function loadLineChartModules() {
return import(/* webpackChunkName: "echarts-line" */ '@/utils/line-chart');
}
async function loadPieChartModules() {
return import(/* webpackChunkName: "echarts-pie" */ '@/utils/pie-chart');
}
/**
* 按功能模块分割
*/
const ChartExporter = lazy(() =>
import(/* webpackChunkName: "chart-exporter", webpackPrefetch: true */ './ChartExporter')
);
const DataTransformer = lazy(() =>
import(/* webpackChunkName: "data-transformer" */ './DataTransformer')
);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
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
优化后的目录结构
src/
├── charts/
│ ├── base/
│ │ └── useECharts.ts # 基础Hook
│ ├── types/
│ │ ├── line.ts # 折线图专用
│ │ ├── bar.ts # 柱状图专用
│ │ └── pie.ts # 饼图专用
│ └── components/
│ ├── LineChart.tsx # 按需加载
│ ├── BarChart.tsx # 按需加载
│ └── PieChart.tsx # 按需加载
├── utils/
│ ├── chart-loader.ts # 动态加载器
│ └── chart-factory.ts # 图表工厂
└── views/
├── Dashboard.tsx # 代码分割
└── Reports.tsx # 代码分割1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
4. Tree Shaking优化
避免副作用
typescript
// ❌ 错误做法 - 有副作用的导入
import echarts from 'echarts';
// 这会导入整个echarts库,包括所有图表类型
// ✅ 正确做法 - 纯函数式导入
import * as echarts from 'echarts/core';
import { LineChart } from 'echarts/charts';
import { GridComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([LineChart, GridComponent, CanvasRenderer]);1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
package.json配置
json
{
"name": "my-echarts-app",
"sideEffects": [
"*.css",
"*.scss"
],
"dependencies": {
"echarts": "^5.4.3"
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
5. 压缩与优化
Terser配置
javascript
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除console
drop_debugger: true, // 移除debugger
pure_funcs: ['console.log'], // 指定移除的函数
passes: 2 // 压缩遍数
},
mangle: {
safari10: true // 兼容Safari 10
},
format: {
comments: false // 移除注释
}
},
extractComments: false,
parallel: true // 并行压缩
})
]
}
};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
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
Gzip/Brotli压缩
javascript
// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');
const zlib = require('zlib');
module.exports = {
plugins: [
// Gzip压缩
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240, // 大于10KB的文件才压缩
minRatio: 0.8, // 压缩率小于0.8才处理
deleteOriginalAssets: false // 保留原文件
}),
// Brotli压缩(更好的压缩率)
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 11
}
},
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false
})
]
};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
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
💡 实战案例
案例1:完整的优化配置
typescript
/**
* 生产环境优化配置
*/
interface OptimizationConfig {
/** 启用的图表类型 */
chartTypes: string[];
/** 启用的组件 */
components: string[];
/** 渲染器 */
renderer: 'canvas' | 'svg';
/** 是否启用动画 */
animation: boolean;
}
class OptimizedChartInitializer {
private static instance: OptimizedChartInitializer | null = null;
private initialized = false;
static getInstance(): OptimizedChartInitializer {
if (!this.instance) {
this.instance = new OptimizedChartInitializer();
}
return this.instance;
}
/**
* 根据配置初始化
*/
async initialize(config: OptimizationConfig): Promise<void> {
if (this.initialized) return;
// 动态构建模块列表
const modules: any[] = [];
// 添加渲染器
if (config.renderer === 'canvas') {
const { CanvasRenderer } = await import('echarts/renderers');
modules.push(CanvasRenderer);
} else {
const { SVGRenderer } = await import('echarts/renderers');
modules.push(SVGRenderer);
}
// 添加图表类型
for (const type of config.chartTypes) {
const module = await import(`echarts/charts/${type}`);
modules.push(module[`${type.charAt(0).toUpperCase()}${type.slice(1)}Chart`]);
}
// 添加组件
for (const comp of config.components) {
const module = await import(`echarts/components/${comp}`);
modules.push(module[`${comp.charAt(0).toUpperCase()}${comp.slice(1)}Component`]);
}
// 注册
const * as echarts = await import('echarts/core');
echarts.use(modules);
// 全局配置
if (!config.animation) {
echarts.registerTheme('no-animation', {
animation: false
});
}
this.initialized = true;
}
/**
* 创建图表
*/
createChart(container: HTMLElement, option: any): echarts.ECharts {
if (!this.initialized) {
throw new Error('Must call initialize() first');
}
const * as echarts = await import('echarts/core');
return echarts.init(container);
}
}
// 使用示例
const initializer = OptimizedChartInitializer.getInstance();
await initializer.initialize({
chartTypes: ['line', 'bar'],
components: ['grid', 'tooltip', 'legend'],
renderer: 'canvas',
animation: true
});
const chart = initializer.createChart(container, option);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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
案例2:微前端架构下的共享
typescript
/**
* 主应用 - 共享ECharts
*/
// main-app/index.ts
import { registerMicroApps, start } from 'qiankun';
// 预加载ECharts到全局
window.__ECHARTS_SHARED__ = await import('echarts/core');
registerMicroApps([
{
name: 'sub-app-1',
entry: '//localhost:8081',
container: '#subapp-1',
activeRule: '/app1',
props: {
echarts: window.__ECHARTS_SHARED__
}
},
{
name: 'sub-app-2',
entry: '//localhost:8082',
container: '#subapp-2',
activeRule: '/app2',
props: {
echarts: window.__ECHARTS_SHARED__
}
}
]);
start();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
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
typescript
/**
* 子应用 - 使用共享的ECharts
*/
// sub-app-1/main.ts
export async function bootstrap(props: any) {
// 使用主应用提供的ECharts
window.echarts = props.echarts;
}
export async function mount(props: any) {
// 只加载需要的图表类型
const { LineChart } = await import('echarts/charts');
window.echarts.use([LineChart]);
renderApp();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
案例3:PWA缓存策略
javascript
// service-worker.js
const CACHE_NAME = 'echarts-v5.4.3';
const ECHARTS_ASSETS = [
'/static/js/echarts-core.[hash].js',
'/static/js/echarts-charts.[hash].js',
'/static/js/echarts-components.[hash].js'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ECHARTS_ASSETS);
})
);
});
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('echarts')) {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
}
});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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
⚠️ 常见问题
问题1:Tree Shaking不生效
症状:打包后仍包含未使用的图表类型
原因:
- 使用了全量引入
- 依赖包没有sideEffects配置
- Babel配置问题
解决:
javascript
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
modules: false // 不要转换ES6模块
}]
]
};1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
问题2:重复打包
症状:多个chunk中包含echarts代码
解决:Webpack提取公共chunk
javascript
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
echarts: {
name: 'echarts-common',
test: /[\\/]node_modules[\\/]echarts/,
priority: 10,
reuseExistingChunk: true
}
}
}
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
🎯 最佳实践
1. 按需引入检查清单
- [ ] 使用
echarts/core而非echarts - [ ] 只注册需要的图表类型
- [ ] 只注册需要的组件
- [ ] 选择合适的渲染器
- [ ] 启用Tree Shaking
- [ ] 配置sideEffects
- [ ] 使用production模式构建
2. Bundle大小监控
yaml
# .size-limit.yml
- path: dist/*.js
limit: 100 KB
gzip: true
- path: dist/echarts-*.js
limit: 50 KB
gzip: true1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
3. 性能预算
| 指标 | 目标 | 说明 |
|---|---|---|
| 初始Bundle | < 100KB | Gzip后 |
| 单个图表类型 | < 20KB | Gzip后 |
| 首屏加载时间 | < 2s | 3G网络 |
| Time to Interactive | < 3.5s | 中端设备 |
📊 优化效果对比
| 阶段 | Bundle大小 | 加载时间 | 优化幅度 |
|---|---|---|---|
| 优化前 | 850KB | 4.2s | - |
| 按需引入 | 300KB | 1.8s | ↓ 65% |
| + Tree Shaking | 220KB | 1.3s | ↓ 74% |
| + Code Splitting | 150KB | 0.9s | ↓ 82% |
| + Gzip | 90KB | 0.6s | ↓ 89% |
🔗 相关链接
💎 总结
代码分割优化是ECharts性能优化的核心环节。通过模块化引入、Tree Shaking、动态加载和合理的压缩策略,可以将包体积从850KB减少到90KB(Gzip后),降幅达89%。记住:最好的代码是不需要执行的代码,按需引入永远是最有效的优化手段。
