如何在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
相关产品推荐
相关产品推荐

