ECharts 构建配置详解
📋 概述
合理的构建配置能够显著优化ECharts应用的打包体积、加载速度和运行时性能。本文档详细介绍Webpack、Vite、Rollup等主流构建工具的ECharts优化配置方案。
核心价值
- ✅ 体积优化:从850KB减少到90KB(Gzip后)
- ✅ 加载加速:首屏加载时间减少80%+
- ✅ 性能提升:Tree Shaking移除未使用代码
- ✅ 缓存优化:合理的Chunk策略提升缓存命中率
🎯 核心概念
构建流程
源码 → 解析(Parser) → 转换(Transform) → 优化(Optimize) → 打包(Bundle) → 压缩(Minify)
↓ ↓ ↓ ↓ ↓ ↓
TypeScript Babel Tree Shaking Code Splitting Terser Gzip/Brotli1
2
3
2
3
关键配置项
| 配置项 | 作用 | 影响 |
|---|---|---|
| mode | 构建模式 | 启用/禁用优化 |
| entry | 入口点 | 决定Chunk数量 |
| output | 输出配置 | 文件名、路径 |
| optimization | 优化配置 | Tree Shaking、分割 |
| plugins | 插件 | 额外优化能力 |
🔧 Webpack配置
1. 基础优化配置
javascript
// webpack.config.js
const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const CompressionPlugin = require('compression-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
// 入口文件
entry: {
app: './src/index.tsx'
},
// 输出配置
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction
? 'js/[name].[contenthash:8].js'
: 'js/[name].js',
chunkFilename: isProduction
? 'js/[name].[contenthash:8].chunk.js'
: 'js/[name].chunk.js',
clean: true, // 清理旧文件
publicPath: '/'
},
// 模块配置
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true, // 加快编译速度
compilerOptions: {
noEmit: false,
sourceMap: true
}
}
}
],
exclude: /node_modules/
},
{
test: /\.jsx?$/,
use: ['babel-loader'],
exclude: /node_modules/
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8KB以下的图片转base64
}
}
}
]
},
// 解析配置
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
// 强制使用ES模块版本
'echarts': 'echarts/dist/echarts.esm.js'
}
},
// 优化配置
optimization: {
// Tree Shaking
usedExports: true,
// 标识模块是否包含副作用
sideEffects: true,
// 压缩配置
minimize: isProduction,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction,
pure_funcs: isProduction ? ['console.log'] : [],
passes: 2
},
mangle: {
safari10: true
},
format: {
comments: false
}
},
extractComments: false,
parallel: true
})
],
// 代码分割
splitChunks: {
chunks: 'all',
maxInitialRequests: 10,
minSize: 20000,
cacheGroups: {
// ECharts核心
echartsCore: {
name: 'echarts-core',
test: /[\\/]node_modules[\\/]echarts[\\/]core/,
priority: 20,
reuseExistingChunk: true
},
// ECharts图表类型
echartsCharts: {
name: 'echarts-charts',
test: /[\\/]node_modules[\\/]echarts[\\/]charts/,
priority: 19,
reuseExistingChunk: true
},
// ECharts组件
echartsComponents: {
name: 'echarts-components',
test: /[\\/]node_modules[\\/]echarts[\\/]components/,
priority: 18,
reuseExistingChunk: true
},
// React/Vue框架
framework: {
name: 'framework',
test: /[\\/]node_modules\\//,
priority: 15,
reuseExistingChunk: true
},
// 通用vendor
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 10,
reuseExistingChunk: true
},
// 共享代码
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
},
// 运行时代码提取
runtimeChunk: {
name: 'runtime'
}
},
// 插件配置
plugins: [
// CSS提取
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css'
}),
// Bundle分析(仅开发环境)
!isProduction && new BundleAnalyzerPlugin({
analyzerMode: 'server',
openAnalyzer: false,
analyzerPort: 8888
}),
// Gzip压缩(仅生产环境)
isProduction && new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false
})
].filter(Boolean),
// 性能提示
performance: {
hints: isProduction ? 'warning' : false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
};
};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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
2. 环境变量配置
javascript
// webpack.config.env.js
const webpack = require('webpack');
const Dotenv = require('dotenv-webpack');
module.exports = {
plugins: [
// 加载.env文件
new Dotenv({
path: './.env',
safe: true,
systemvars: true
}),
// 定义全局常量
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.ECHARTS_VERSION': JSON.stringify('5.4.3'),
'__DEV__': JSON.stringify(process.env.NODE_ENV !== 'production'),
'__PROD__': JSON.stringify(process.env.NODE_ENV === 'production')
})
]
};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
typescript
// src/config/chart.config.ts
/**
* 根据环境变量配置图表模块
*/
export const CHART_CONFIG = {
// 开发环境:全量引入便于调试
...(process.env.NODE_ENV === 'development' && {
types: ['line', 'bar', 'pie', 'scatter', 'radar'],
components: ['grid', 'tooltip', 'legend', 'title', 'dataZoom'],
renderer: 'canvas'
}),
// 生产环境:按需引入
...(process.env.NODE_ENV === 'production' && {
types: process.env.CHART_TYPES?.split(',') || ['line', 'bar'],
components: process.env.CHART_COMPONENTS?.split(',') || ['grid', 'tooltip'],
renderer: (process.env.CHART_RENDERER as 'canvas' | 'svg') || 'canvas'
})
};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
3. 多线程构建
javascript
// webpack.config.thread.js
const ThreadJsPlugin = require('thread-loader');
module.exports = {
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
poolTimeout: 200
}
},
'ts-loader'
],
include: path.resolve(__dirname, 'src')
}
]
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
🔧 Vite配置
1. 基础优化配置
typescript
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
import compression from 'vite-plugin-compression';
import legacy from '@vitejs/plugin-legacy';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
const isProduction = mode === 'production';
return {
// 插件配置
plugins: [
react(),
// Gzip压缩
compression({
verbose: true,
disable: !isProduction,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz'
}),
// Brotli压缩
compression({
verbose: true,
disable: !isProduction,
threshold: 10240,
algorithm: 'brotliCompress',
ext: '.br'
}),
// Bundle可视化
isProduction && visualizer({
open: true,
gzipSize: true,
brotliSize: true,
filename: 'dist/stats.html'
}),
// 传统浏览器支持
legacy({
targets: ['defaults', 'not IE 11'],
modernPolyfills: true
})
],
// 构建配置
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: !isProduction,
minify: 'terser',
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction,
pure_funcs: isProduction ? ['console.log'] : []
}
},
// 代码分割
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes('node_modules')) {
// ECharts分包
if (id.includes('echarts/core')) {
return 'echarts-core';
}
if (id.includes('echarts/charts')) {
return 'echarts-charts';
}
if (id.includes('echarts/components')) {
return 'echarts-components';
}
// 框架分包
if (id.includes('react') || id.includes('react-dom')) {
return 'framework';
}
// 其他vendor
return 'vendor';
}
},
// Chunk文件名
chunkFileNames: (chunkInfo) => {
const facadeModuleId = chunkInfo.facadeModuleId
? chunkInfo.facadeModuleId.split('/').pop()
: 'chunk';
return `js/${facadeModuleId}-[hash].js`;
}
}
},
// Chunk大小警告阈值
chunkSizeWarningLimit: 500
},
// 依赖预构建
optimizeDeps: {
include: [
'echarts/core',
'echarts/charts',
'echarts/components'
],
exclude: []
},
// 服务器配置
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
},
// CSS配置
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true
}
},
postcss: {
plugins: [
require('autoprefixer')({
overrideBrowserslist: ['> 1%', 'last 2 versions', 'not dead']
})
]
}
}
};
});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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
2. 自定义ECharts插件
typescript
// plugins/vite-plugin-echarts-optimize.ts
import type { Plugin } from 'vite';
interface EChartsOptimizeOptions {
/** 启用的图表类型 */
chartTypes?: string[];
/** 启用的组件 */
components?: string[];
/** 是否启用Tree Shaking */
treeShaking?: boolean;
}
export function viteEChartsOptimize(options: EChartsOptimizeOptions = {}): Plugin {
const {
chartTypes = ['line', 'bar'],
components = ['grid', 'tooltip'],
treeShaking = true
} = options;
return {
name: 'vite-echarts-optimize',
enforce: 'pre',
config(config) {
return {
resolve: {
alias: {
// 指向ES模块版本
'echarts$': 'echarts/dist/echarts.esm.js'
}
},
optimizeDeps: {
include: [
'echarts/core',
...chartTypes.map(type => `echarts/charts/${type}`),
...components.map(comp => `echarts/components/${comp}`)
]
}
};
},
transform(code, id) {
if (!treeShaking) return null;
// 检测全量引入
if (code.match(/import\s+\*\s+as\s+echarts\s+from\s+['"]echarts['"]/)) {
this.warn({
message: 'Detected full import of echarts. Consider using modular imports.',
id
});
}
return null;
}
};
}
// 使用
// vite.config.ts
import { viteEChartsOptimize } from './plugins/vite-plugin-echarts-optimize';
export default defineConfig({
plugins: [
viteEChartsOptimize({
chartTypes: ['line', 'bar', 'pie'],
components: ['grid', 'tooltip', 'legend'],
treeShaking: 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
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
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
🔧 Rollup配置
javascript
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
import visualizer from 'rollup-plugin-visualizer';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true
},
{
file: 'dist/index.umd.js',
format: 'umd',
name: 'MyChartLibrary',
sourcemap: true,
globals: {
react: 'React',
'react-dom': 'ReactDOM',
echarts: 'echarts'
}
}
],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.json' }),
terser({
compress: {
drop_console: true,
drop_debugger: true
}
}),
visualizer({
filename: 'dist/stats.html',
gzipSize: true
})
],
external: ['react', 'react-dom', 'echarts/core'],
treeshake: {
preset: 'recommended',
moduleSideEffects: false,
propertyReadSideEffects: 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
💡 实战案例
案例1:Monorepo配置
javascript
// pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'1
2
3
4
2
3
4
typescript
// packages/shared-chart/vite.config.ts
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
name: 'SharedChart',
fileName: (format) => `shared-chart.${format}.js`
},
rollupOptions: {
external: ['react', 'echarts/core'],
output: {
globals: {
react: 'React',
'echarts/core': 'echarts'
}
}
}
},
plugins: [
dts({
insertTypesEntry: 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
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
案例2:CI/CD优化
yaml
# .github/workflows/build.yml
name: Build and Optimize
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build with analysis
run: pnpm build:analyze
env:
ANALYZE: true
- name: Check bundle size
uses: preactjs/compressed-size-action@v2
with:
pattern: './dist/**/*.js'
maximumSize: 100000 # 100KB
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/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
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
⚠️ 常见问题
问题1:Source Map在生产环境泄露
症状:生产环境可以查看源码
解决:
javascript
// webpack.config.js
module.exports = {
// 生产环境不生成source map
devtool: isProduction ? false : 'source-map'
};1
2
3
4
5
2
3
4
5
问题2:HMR失效
症状:修改代码后页面不热更新
解决:
javascript
// webpack.config.js
module.exports = {
devServer: {
hot: true,
liveReload: false
}
};1
2
3
4
5
6
7
2
3
4
5
6
7
🎯 最佳实践
1. 构建配置检查清单
- [ ] 启用Tree Shaking
- [ ] 配置sideEffects
- [ ] 代码分割合理
- [ ] 启用长期缓存(contenthash)
- [ ] 压缩JS/CSS
- [ ] 启用Gzip/Brotli
- [ ] 配置alias优化
- [ ] 排除node_modules
- [ ] 设置performance budget
2. 性能监控
javascript
// webpack-build-time-plugin.js
class BuildTimePlugin {
apply(compiler) {
let startTime;
compiler.hooks.run.tap('BuildTimePlugin', () => {
startTime = Date.now();
});
compiler.hooks.done.tap('BuildTimePlugin', (stats) => {
const buildTime = Date.now() - startTime;
console.log(`Build completed in ${buildTime}ms`);
// 如果构建时间超过阈值,发出警告
if (buildTime > 30000) {
console.warn('Build time exceeded 30s!');
}
});
}
}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
📊 构建性能对比
| 优化项 | 构建时间 | Bundle大小 | 提升 |
|---|---|---|---|
| 初始 | 45s | 850KB | - |
| Tree Shaking | 42s | 300KB | ↓ 65% |
| Code Splitting | 40s | 220KB | ↓ 74% |
| Terser压缩 | 45s | 150KB | ↓ 82% |
| Gzip | 48s | 90KB | ↓ 89% |
🔗 相关链接
💎 总结
构建配置是ECharts性能优化的基础设施。通过合理的代码分割、Tree Shaking、压缩和缓存策略,可以将构建时间从45秒优化到48秒(增加插件开销),但Bundle大小从850KB减少到90KB,降幅达89%。记住:构建优化是一个持续的过程,需要定期分析和调整配置。
