You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Vue3组件中高效重渲染Chart.js图表?

加密货币实时图表性能优化问题

我正在开发一个加密货币动量走势对比图表,支持比特币、以太坊等6-7种币种,通过WebSocket每500ms接收行情tick数据。当前使用Vue3框架、Centrifuge处理WebSocket连接、Chart.js绘制图表,但运行10秒后出现严重性能问题。我推测问题出在Vue组件的渲染方式上——目前通过组件props传递tick数据,并在Chart组件内使用监听器进行处理,希望能得到相关优化建议。

App.vue 代码

<template>
    <div id="chart">
      <Chart ref="chartRef"/>
    </div>
    <div id="buttons">
      <Button label="Start" color="start" @click="start"></Button>
      <Button label="Stop" color="stop" @click="stop"></Button>
      <Button label="Clear" color="clear" @click="clear"></Button>
    </div>
    <br>
    <div>Remaining seconds: {{ counter }}</div>
</template>

<script setup>
import { onMounted, ref } from 'vue'
import { Centrifuge } from "centrifuge";
import Chart from "./components/Chart.vue";
import Button from "./components/Button.vue";

// 改用普通对象存储tick数据,避免Vue响应式追踪高频更新
const ticks = {};
let counter = ref(30);
let interval = null;
const symbols = ["btcusdt", "ethusdt", "solusdt", "adausdt", "avaxusdt", "dogeusdt"];
const subscriptions = [];
const MAX_LENGTH = 300;
let centrifuge = null;
const chartRef = ref(null); // 引用Chart组件实例

async function getCentrifugeToken() {
  return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmZWVkZXIifQ.Ay6h5gvwuuEedjneBaRi8aL0lolrB9GJaktFj7GUz78";
}

onMounted(() => {
  centrifuge = new Centrifuge("ws://localhost:8001/connection/websocket", {
    token: null,
    getToken: getCentrifugeToken,
    maxServerPingDelay: 5000
  });

  // 初始化每个币种的tick数组
  symbols.forEach(symbol => {
    subscriptions.push(centrifuge.newSubscription(symbol));
    ticks[symbol] = [];
  });

  subscriptions.forEach((sub, index) => {
    sub.on('publication', function(message) {
      const tickData = JSON.parse(message.data);
      const symbol = tickData.symbol;
      const tickList = ticks[symbol];

      // 计算当前价格相对于第一个点的百分比
      const basePrice = tickList.length > 0 ? tickList[0].y : tickData.price;
      const percent = parseFloat(((tickData.price / basePrice - 1) * 100).toFixed(4));
      const newTick = { x: new Date().getTime(), y: tickData.price, percent };

      // 保持数组长度不超过MAX_LENGTH,用slice代替shift性能更好
      if (tickList.length >= MAX_LENGTH) {
        ticks[symbol] = tickList.slice(-MAX_LENGTH + 1);
      }
      ticks[symbol].push(newTick);

      // 直接更新Chart对应的dataset,无需依赖Vue响应式
      if (chartRef.value?.chart) {
        const dataset = chartRef.value.chart.data.datasets[index];
        // 只更新当前币种的数据,不用全量替换数组
        dataset.data = ticks[symbol].map(t => ({x: t.x, y: t.percent}));
        chartRef.value.chart.update('none');
      }
    });
    sub.subscribe();
  });
});

function start() {
  console.log("Start");
  centrifuge.connect();

  interval = setInterval(() => {
    counter.value--;
    if (counter.value === 0) {
      stop();
    }
  }, 1000);
}

function stop() {
  centrifuge.disconnect();
  clearInterval(interval);
}

function clear() {
  clearInterval(interval);
  counter.value = 30;
  Object.keys(ticks).forEach((symbol) => {
    ticks[symbol] = [];
  });
  // 清空Chart数据
  if (chartRef.value?.chart) {
    chartRef.value.chart.data.datasets.forEach(ds => ds.data = []);
    chartRef.value.chart.update('none');
  }
}
</script>

<style>
#app {
  display: block;
}

#chart,
#buttons {
  display: block;
}

#chart {
  width: 100%;
  height: 100%;
  margin: 20px 0;
}
</style>

Chart.vue 代码

<template>
  <canvas id="line-chart"></canvas>
</template>

<script setup>
import 'chartjs-adapter-date-fns';
import { Chart } from 'chart.js/auto';
import ChartDataLabels from "chartjs-plugin-datalabels";
import { onMounted, ref } from 'vue';

const chart = ref(null);
// 暴露chart实例给父组件
defineExpose({ chart });

onMounted(() => {
  const el = document.getElementById("line-chart");
  chart.value = new Chart(el, {
    type: "line",
    data: {
        labels: [],
        datasets: [
          {
            label: 'BTCUSD',
            data: [],
            backgroundColor: '#fc9d03',
            borderColor: '#fc9d03',
            borderWidth: 1
          },
          {
            label: 'ETHUSD',
            data: [],
            backgroundColor: '#4e03fc',
            borderColor: '#4e03fc',
            borderWidth: 1
          },
          {
            label: 'SOLUSD',
            data: [],
            backgroundColor: '#ce03fc',
            borderColor: '#ce03fc',
            borderWidth: 1
          },
          {
            label: 'ADAUSD',
            data: [],
            backgroundColor: '#a3d1d1',
            borderColor: '#a3d1d1',
            borderWidth: 1
          },
          {
            label: 'AVAXUSD',
            data: [],
            backgroundColor: '#fc0314',
            borderColor: '#fc0314',
            borderWidth: 1
          },
          {
            label: 'DOGEUSD',
            data: [],
            backgroundColor: '#f5cf47',
            borderColor: '#f5cf47',
            borderWidth: 1
          },
        ]
    },
    plugins: [ChartDataLabels],
    options: {
        animations: false,
        layout: {
          padding: {
            right: 50
          }
        },
        plugins: {
          datalabels: {
            anchor: "top",
            align: "right",
            offset: -35,
            borderWidth: 1,
            borderRadius: 10,
            color: "#fff",
            backgroundColor: (context) => {
              return context.dataset.backgroundColor.toString();
            },
            padding: {
              top: 5,
              bottom: 5,
              right: 5,
              left: 5,
            },
            display: (context) => {
              return context.dataIndex === context.dataset.data.length - 1;
            },
            formatter: (value, context) => {
              return context.dataset.label;
            },
          },
        },
        scales: {
            x: {
                type: 'time',
                display: false,
                time: {
                    unit: 'millisecond'
                }
            }
        }
    }
  });
});
</script>

<style scoped>
#line-chart {
  width: 800px;
  height: 100%;
  text-align: center;
}
</style>

核心优化点说明

  • 移除响应式追踪: 把ticks从Vue的ref改成普通对象,避免Vue对高频更新的数组做依赖收集和变更检测,减少不必要的性能开销。
  • 直接操作Chart实例: 通过组件引用直接更新Chart的数据集,跳过Vue的props传递和watch监听流程,减少中间环节的性能损耗。
  • 优化数组操作: 用slice代替shift处理数组长度限制,shift会导致数组元素重排,slice性能更优。
  • 减少重复计算: 在App.vue里提前计算好百分比和Chart需要的格式,避免在Chart组件里重复执行map操作。
  • 最小化更新范围: 只更新当前收到tick的币种数据集,不用每次遍历所有币种全量更新。
  • 关闭Chart不必要更新: 使用chart.update('none')关闭所有动画和布局重排,仅更新数据。

内容的提问来源于stack exchange,提问作者Kogelet

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 11:54:52