在 Vue 中使用 ECharts 渲染 10 万条数据
·
在 Vue 中使用 ECharts 渲染 10 万条数据时,直接全部渲染会导致严重的性能问题。我们需要采用一些优化策略来处理这种大数据量的场景。
以下是一个高效处理 10 万条数据的 Vue 组件实现,主要采用了数据采样、分片加载和 ECharts 的大数据优化配置:
<template>
<div class="big-data-chart">
<div ref="chartRef" class="chart-container"></div>
<div class="loading" v-if="loading">
<div class="spinner"></div>
<p>正在处理数据... {{ progress }}%</p>
</div>
<div class="controls">
<button @click="handleRenderAll" :disabled="loading">渲染全部数据</button>
<button @click="handleRenderSampled" :disabled="loading">渲染采样数据</button>
<button @click="handleRenderChunked" :disabled="loading">分片加载数据</button>
<label>
<input type="checkbox" v-model="useWebWorker" :disabled="loading">
使用Web Worker处理数据
</label>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
import { generateLargeData } from './dataGenerator'; // 假设的大数据生成工具
// 状态变量
const chartRef = ref(null);
const chartInstance = ref(null);
const loading = ref(false);
const progress = ref(0);
const useWebWorker = ref(true);
const processedData = ref(null);
// 初始化图表
const initChart = () => {
if (chartInstance.value) {
chartInstance.value.dispose();
}
chartInstance.value = echarts.init(chartRef.value);
// 配置ECharts的大数据优化
chartInstance.value.setOption({
animation: false, // 关闭动画提升性能
progressive: 2000, // 渐进式渲染,每次渲染2000个点
progressiveThreshold: 5000, // 超过5000个点启用渐进式渲染
large: true, // 启用大数据模式
largeThreshold: 2000, // 大数据模式阈值
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line'
}
},
xAxis: {
type: 'category',
show: false // 数据量太大时可以隐藏坐标轴标签
},
yAxis: {
type: 'value'
},
series: [{
type: 'line',
symbol: 'none', // 不显示数据点
lineStyle: {
width: 1
},
data: []
}]
});
// 监听窗口大小变化,调整图表
window.addEventListener('resize', handleResize);
};
// 处理窗口大小变化
const handleResize = () => {
if (chartInstance.value) {
chartInstance.value.resize();
}
};
// 生成10万条原始数据
const generateData = () => {
return generateLargeData(100000);
};
// 使用Web Worker处理数据
const processDataWithWorker = (rawData) => {
return new Promise((resolve) => {
// 创建Web Worker
const worker = new Worker(new URL('./dataProcessor.worker.js', import.meta.url));
// 监听Worker消息
worker.onmessage = (e) => {
if (e.data.type === 'progress') {
progress.value = e.data.progress;
} else if (e.data.type === 'result') {
worker.terminate(); // 完成后终止Worker
resolve(e.data.data);
}
};
// 发送数据给Worker处理
worker.postMessage(rawData);
});
};
// 主线程处理数据
const processDataInMainThread = (rawData) => {
return new Promise((resolve) => {
// 简单处理,实际场景可能更复杂
const result = rawData.map((item, index) => {
// 每处理1000条数据更新一次进度
if (index % 1000 === 0) {
progress.value = Math.floor((index / rawData.length) * 100);
}
return item;
});
progress.value = 100;
resolve(result);
});
};
// 数据采样处理 - 只取部分数据点
const sampleData = (rawData, sampleRate = 0.1) => {
const sampled = [];
const step = Math.ceil(1 / sampleRate);
for (let i = 0; i < rawData.length; i += step) {
sampled.push(rawData[i]);
// 更新进度
if (i % 10000 === 0) {
progress.value = Math.floor((i / rawData.length) * 100);
}
}
progress.value = 100;
return sampled;
};
// 分片加载数据
const renderDataInChunks = async (processedData) => {
const chunkSize = 5000; // 每次加载5000条
const totalChunks = Math.ceil(processedData.length / chunkSize);
// 先清空现有数据
chartInstance.value.setOption({
series: [{ data: [] }]
});
// 分块加载
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, processedData.length);
const chunk = processedData.slice(start, end);
// 获取当前已有的数据
const currentData = chartInstance.value.getOption().series[0].data || [];
// 追加新数据
chartInstance.value.setOption({
series: [{
data: [...currentData, ...chunk]
}]
});
// 更新进度
progress.value = Math.floor(((i + 1) / totalChunks) * 100);
// 每加载一块,给浏览器一点时间处理其他任务
await new Promise(resolve => setTimeout(resolve, 50));
}
};
// 渲染全部数据
const handleRenderAll = async () => {
loading.value = true;
progress.value = 0;
try {
// 生成原始数据
const rawData = generateData();
progress.value = 20;
// 处理数据
let dataToRender;
if (useWebWorker.value) {
dataToRender = await processDataWithWorker(rawData);
} else {
dataToRender = await processDataInMainThread(rawData);
}
// 渲染数据
chartInstance.value.setOption({
series: [{
data: dataToRender
}]
});
processedData.value = dataToRender;
} catch (error) {
console.error('渲染数据失败:', error);
} finally {
loading.value = false;
}
};
// 渲染采样数据
const handleRenderSampled = async () => {
loading.value = true;
progress.value = 0;
try {
// 生成原始数据
const rawData = generateData();
progress.value = 20;
// 采样处理 (取10%的数据)
const sampled = sampleData(rawData, 0.1);
// 渲染数据
chartInstance.value.setOption({
series: [{
data: sampled
}]
});
processedData.value = sampled;
} catch (error) {
console.error('渲染采样数据失败:', error);
} finally {
loading.value = false;
}
};
// 分片加载数据
const handleRenderChunked = async () => {
if (!processedData.value) {
loading.value = true;
progress.value = 0;
try {
// 生成并处理数据
const rawData = generateData();
progress.value = 20;
processedData.value = useWebWorker.value
? await processDataWithWorker(rawData)
: await processDataInMainThread(rawData);
} catch (error) {
console.error('处理数据失败:', error);
loading.value = false;
return;
}
}
// 分块渲染
try {
await renderDataInChunks(processedData.value);
} catch (error) {
console.error('分片渲染失败:', error);
} finally {
loading.value = false;
}
};
// 组件挂载时初始化图表
onMounted(() => {
nextTick(() => {
initChart();
});
});
// 组件卸载时清理
watch(chartRef, (newVal, oldVal) => {
if (!newVal && chartInstance.value) {
chartInstance.value.dispose();
window.removeEventListener('resize', handleResize);
}
});
</script>
<style scoped>
.big-data-chart {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
}
.chart-container {
width: 100%;
height: 600px;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.8);
padding: 20px;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.controls {
display: flex;
gap: 12px;
align-items: center;
}
button {
padding: 8px 16px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background-color: #359e75;
}
label {
display: flex;
align-items: center;
gap: 6px;
margin-left: auto;
}
</style>
上面的组件实现了几种处理 10 万条数据的策略,主要优化点包括:
-
ECharts 配置优化:
- 关闭动画提升性能
- 启用渐进式渲染和大数据模式
- 隐藏数据点 (symbol: 'none') 减少绘制压力
- 必要时隐藏坐标轴标签
-
数据处理策略:
- 数据采样:只渲染部分代表性数据点
- 分片加载:将数据分成小块逐步渲染
- Web Worker:使用 Worker 处理数据,避免阻塞主线程
-
用户体验优化:
- 加载进度显示
- 提供不同渲染策略的选择
- 响应式图表大小调整
使用时,还需要创建数据生成器和 Web Worker 文件:
// dataGenerator.js
export const generateLargeData = (count) => {
const data = [];
let value = 50;
for (let i = 0; i < count; i++) {
// 生成有一定趋势的随机数据
value += Math.random() * 20 - 10;
data.push({
name: `Point ${i}`,
value: [i, Math.max(0, Math.min(100, value))]
});
}
return data;
};
// dataProcessor.worker.js
self.onmessage = (e) => {
const rawData = e.data;
const result = [];
// 在Worker中处理数据,例如做一些复杂计算
for (let i = 0; i < rawData.length; i++) {
// 模拟数据处理
result.push(rawData[i].value);
// 每处理1000条数据,发送进度更新
if (i % 1000 === 0) {
self.postMessage({
type: 'progress',
progress: Math.floor((i / rawData.length) * 100)
});
}
}
// 处理完成,发送结果
self.postMessage({
type: 'result',
data: result
});
};
对于 10 万条数据的场景,建议优先使用数据采样或分片加载的方式,在保证视觉效果的同时大大提升性能。如果确实需要展示全部数据,可以结合 Web Worker 和 ECharts 的大数据优化配置来实现。
更多推荐


所有评论(0)