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

React Canvas实时图表更新闪烁问题 附代码求解决方案

React Canvas实时图表闪烁问题解决方法

问题现象

使用React结合HTML Canvas开发实时图表时,数据每100ms更新一次,图表出现明显闪烁,演示效果如下:
图表闪烁演示

问题根源

  1. 清屏与重绘的间隙:每次数据更新时先调用clearRect清空画布,再重新绘制线条,两个操作间存在视觉空隙,导致闪烁。
  2. 绘制逻辑冗余:多次重复调用clearRect,且线条绘制时频繁使用moveTo打断路径连贯性,降低绘制效率。
  3. Canvas尺寸管理缺失:仅在组件初始化时设置Canvas尺寸,未监听容器尺寸变化,可能导致画布拉伸或重绘异常。
  4. 绘制时机不同步:直接在useEffect中执行绘制,未与浏览器刷新帧同步,导致绘制时机与屏幕渲染错位。

解决方案

1. 采用离屏Canvas(双缓冲)

先在内存中的离屏Canvas完成全部绘制,再一次性将结果复制到显示Canvas,避免清屏和重绘的视觉间隙。

2. 优化绘制逻辑

  • 减少clearRect调用次数,仅在离屏Canvas初始化时清屏。
  • 连贯绘制路径:去掉多余的moveTo,用lineTo连续绘制线条,提升绘制效率。

3. 同步Canvas尺寸

使用ResizeObserver监听容器尺寸变化,实时更新Canvas的实际宽高(canvas.width/canvas.height),避免样式尺寸与实际画布尺寸不匹配导致的重绘问题。

4. 与浏览器渲染帧同步

确保绘制操作与浏览器刷新节奏一致,避免绘制时机错位。


修改后的代码

父组件(CanvasLineChartDemo)

import CanvasLine from '@components/CanvasLine';
import { memo, useEffect, useState } from 'react';

const CHART_DUMMY_DATA2 = (): Array<number> => {
    return [...new Array(249)].map(() => 0);
};

const CanvasLineChartDemo = () => {
    const [chartHRArray, setChartHRArray] = useState(CHART_DUMMY_DATA2);
    const [chartSP02Array, setChartSP02Array] = useState(CHART_DUMMY_DATA2);
    const [chartRESPArray, setChartRESPArray] = useState(CHART_DUMMY_DATA2);

    const returnRandomFloat = (min: number, max: number) => {
        return parseFloat((Math.random() * (max - min) + min).toFixed(2));
    };

    const validate = () => {
        setChartHRArray(prev => [...prev, returnRandomFloat(-100, 100)].slice(1));
        setChartSP02Array(prev => [...prev, returnRandomFloat(-100, 100)].slice(1));
        setChartRESPArray(prev => [...prev, returnRandomFloat(-100, 100)].slice(1));
    };

    useEffect(() => {
        const interval = setInterval(() => {
            validate();
        }, 100);

        return () => clearInterval(interval);
    }, []);

    return (
        <div style={{ width: '700px', background: '#000', marginRight: 10 }}>
            <CanvasLine height={50} canvasData={chartHRArray} />
            <CanvasLine height={50} canvasData={chartSP02Array} />
            <CanvasLine height={50} canvasData={chartRESPArray} />
        </div>
    );
};
export default memo(CanvasLineChartDemo);

子组件(CanvasLine)

import React, { useEffect, useRef } from 'react';

type CanvasType = {
    yMin?: number;
    yMax?: number;
    height: number;
    lineColor: string;
    canvasData: number[];
};

const CanvasLine = ({
    yMax,
    yMin,
    height,
    lineColor = '#2C82C9',
    canvasData,
}: CanvasType) => {
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const offscreenCanvasRef = useRef<HTMLCanvasElement | null>(null);

    // 获取数组最大/最小值
    const getYMax = (data: number[]) => Math.max(...data);
    const getYMin = (data: number[]) => Math.min(...data);

    // 计算Y轴坐标(适配Canvas左上角原点)
    const calculateY = (value: number, canvasHeight: number, yMaxNum: number, yMinNum: number) => {
        const range = yMaxNum - yMinNum;
        // 处理所有数据相同的边界情况
        if (range === 0) return canvasHeight / 2;
        const yScale = canvasHeight / range;
        return canvasHeight - ((value - yMinNum) * yScale);
    };

    // 更新Canvas及离屏Canvas尺寸
    const updateCanvasSize = () => {
        const canvas = canvasRef.current;
        if (!canvas) return;
        
        const containerWidth = canvas.offsetWidth;
        canvas.width = containerWidth;
        canvas.height = height;
        canvas.style.height = `${height}px`;

        const offscreenCanvas = offscreenCanvasRef.current;
        if (offscreenCanvas) {
            offscreenCanvas.width = containerWidth;
            offscreenCanvas.height = height;
        }
    };

    // 初始化Canvas与监听尺寸变化
    useEffect(() => {
        offscreenCanvasRef.current = document.createElement('canvas');
        updateCanvasSize();

        const resizeObserver = new ResizeObserver(() => updateCanvasSize());
        canvasRef.current && resizeObserver.observe(canvasRef.current);

        return () => {
            resizeObserver.disconnect();
            offscreenCanvasRef.current = null;
        };
    }, [height]);

    // 数据更新时执行绘制
    useEffect(() => {
        const canvas = canvasRef.current;
        const offscreenCanvas = offscreenCanvasRef.current;
        if (!canvas || !offscreenCanvas || canvasData.length === 0) return;

        const ctx = canvas.getContext('2d');
        const offscreenCtx = offscreenCanvas.getContext('2d');
        if (!ctx || !offscreenCtx) return;

        const yMaxNum = yMax ?? getYMax(canvasData);
        const yMinNum = yMin ?? getYMin(canvasData);
        const canvasWidth = canvas.width;
        const canvasHeight = canvas.height;
        const xStep = canvasWidth / (canvasData.length - 1);

        // 清空离屏画布
        offscreenCtx.clearRect(0, 0, canvasWidth, canvasHeight);

        // 设置绘制样式
        offscreenCtx.lineWidth = 1.5;
        offscreenCtx.lineJoin = 'round';
        offscreenCtx.lineCap = 'round';
        offscreenCtx.strokeStyle = lineColor;

        // 开始绘制连续路径
        offscreenCtx.beginPath();
        const firstY = calculateY(canvasData[0], canvasHeight, yMaxNum, yMinNum);
        offscreenCtx.moveTo(0, firstY);

        for (let i = 1; i < canvasData.length; i++) {
            const x = i * xStep;
            const y = calculateY(canvasData[i], canvasHeight, yMaxNum, yMinNum);
            offscreenCtx.lineTo(x, y);
        }

        offscreenCtx.stroke();

        // 将离屏画布内容复制到显示画布
        ctx.clearRect(0, 0, canvasWidth, canvasHeight);
        ctx.drawImage(offscreenCanvas, 0, 0);
    }, [canvasData, yMax, yMin, lineColor, height]);

    return <canvas ref={canvasRef} style={{ width: '100%', display: 'block' }} />;
};

export default React.memo(CanvasLine);

CanvasLine.defaultProps = {
    height: 50,
    lineColor: '#2C82C9',
    canvasData: [],
};

关键优化说明

  1. 离屏Canvas:所有绘制操作先在内存中完成,最后一次性复制到显示Canvas,彻底消除清屏与重绘的视觉间隙。
  2. 连贯路径绘制:去掉多余的moveTo指令,用连续的lineTo绘制线条,减少绘制开销。
  3. 动态尺寸监听:使用ResizeObserver实时同步画布尺寸,避免容器变化导致的画布拉伸或重绘异常。
  4. 边界情况处理:修复Y轴计算逻辑,处理所有数据相同的场景,确保图表显示正常。
  5. 类型修正:将随机数返回值从字符串转为数字,避免绘制时的类型错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 21:05:23