在 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 万条数据的策略,主要优化点包括:

  1. ECharts 配置优化

    • 关闭动画提升性能
    • 启用渐进式渲染和大数据模式
    • 隐藏数据点 (symbol: 'none') 减少绘制压力
    • 必要时隐藏坐标轴标签
  2. 数据处理策略

    • 数据采样:只渲染部分代表性数据点
    • 分片加载:将数据分成小块逐步渲染
    • Web Worker:使用 Worker 处理数据,避免阻塞主线程
  3. 用户体验优化

    • 加载进度显示
    • 提供不同渲染策略的选择
    • 响应式图表大小调整

使用时,还需要创建数据生成器和 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 的大数据优化配置来实现。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐