DEV Community

TusharIbtekar
TusharIbtekar

Posted on

Building a High-Performance Real-Time Chart in React: Lessons Learned

Real-time data visualization is tricky. While many tutorials show you how to create basic charts, they often skip over the challenges of handling continuous data streams efficiently. Here's how I built a production-ready solution that handles thousands of data points while maintaining smooth performance.

The Common Pitfalls

Most basic real-time chart implementations look something like this:

const BasicChart = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    // DON'T DO THIS
    websocket.on('newData', (value) => {
      setData(prev => [...prev, value]);
      chartRef.current?.update();
    });
  }, []);

  return <Line data={data} />;
};
Enter fullscreen mode Exit fullscreen mode

This approach has several problems:

  • Every data point triggers a re-render
  • Memory usage grows indefinitely
  • Chart animations cause jank
  • CPU usage spikes with frequent updates

A Better Architecture

The actual implementation uses a combination of WebSocket updates and REST API calls for initial data loading. This is a generalized version.

1. Smart Data Management

First, let's define our constants and types:

// constants.ts
export const CHART_CONFIG = {
  UPDATE_DELAY: 100,        // Debounce delay in ms
  INITIAL_RANGE: 600000,    // Initial time window (10 minutes)
  POINT_LIMIT: 1000,        // Maximum points to fetch initially
  POINT_THRESHOLD: 3000,    // Threshold before data cleanup
  CHANGE_THRESHOLD: 1       // Minimum change to record new point
};

interface TimeseriesData {
  metric: string;
  values: number[];
  timestamps: number[];
}
Enter fullscreen mode Exit fullscreen mode

2. Intelligent Data Processing

The processNewDataPoint function handles both new WebSocket data and existing metrics
Here's how we handle new data points:

const processNewDataPoint = (
  existingData: TimeseriesData,
  newValue: number
): void => {
  const lastValue = existingData.values[existingData.values.length - 1];

  // Only record significant changes
  const hasSignificantChange = Math.abs(newValue - lastValue) > CHART_CONFIG.CHANGE_THRESHOLD;

  if (hasSignificantChange) {
    existingData.values.push(newValue);
    existingData.timestamps.push(Date.now());
  } else {
    // Update the last timestamp without adding duplicate data
    existingData.timestamps[existingData.timestamps.length - 1] = Date.now();
  }
};
Enter fullscreen mode Exit fullscreen mode

3. Optimized Chart Component

The chart updates are optimized using chartRef.current?.update('none') to skip animations.
Here's our main chart component with performance optimizations:

const RealTimeChart: FC<RealTimeChartProps> = ({ metrics, dataSource }) => {
  const chartRef = useRef<ChartJS>();
  const [activeMetrics, setActiveMetrics] = useState<string[]>([]);

  // Debounced chart update
  const updateChart = useCallback(
    debounce(() => {
      chartRef.current?.update('none');
    }, CHART_CONFIG.UPDATE_DELAY),
    []
  );

  // Cleanup the chart instance and debounced function when the component unmounts
    useEffect(() => {
        return () => {
            // Cleanup the chart instance
            if (chartRef.current) {
                chartRef.current.destroy();
            }
            // Cleanup the debounced function
            updateChart.cancel();
        };
    }, [updateChart]);

  useEffect(() => {
    if (!dataSource) return;

    const handleNewData = (newData: MetricUpdate) => {
      // Process each metric
      metrics.forEach(metric => {
        if (!activeMetrics.includes(metric.id)) return;

        processNewDataPoint(metric, newData[metric.id]);
      });

      // Check if we need to clean up old data
      const shouldCleanup = metrics.some(
        metric => metric.values.length > CHART_CONFIG.POINT_THRESHOLD
      );

      if (shouldCleanup) {
        cleanupHistoricalData();
      }

      updateChart();
    };

    dataSource.subscribe(handleNewData);
    return () => dataSource.unsubscribe(handleNewData);
  }, [metrics, activeMetrics, dataSource]);

  return (
    <ChartContainer>
      <Line
        ref={chartRef}
        data={getChartData(metrics, activeMetrics)}
        options={getChartOptions()}
      />
      <MetricSelector
        metrics={metrics}
        active={activeMetrics}
        onChange={setActiveMetrics}
      />
    </ChartContainer>
  );
};
Enter fullscreen mode Exit fullscreen mode

4. Performance-Optimized

Chart Configuration

export const getChartOptions = (): ChartOptions<'line'> => ({
  responsive: true,
  maintainAspectRatio: false,

  // Optimize animations
  animation: false,

  scales: {
    x: {
      type: 'time',
      time: {
        unit: 'minute',
        displayFormats: {
          minute: 'HH:mm'
        }
      },
      // Optimize tick display
      ticks: {
        maxTicksLimit: 8,
        source: 'auto'
      }
    },
    y: {
      type: 'linear',
      // Optimize grid lines
      grid: {
        drawBorder: false,
        drawTicks: false
      }
    }
  },

  elements: {
    // Disable points for performance
    point: {
      radius: 0
    },
    line: {
      tension: 0.3,
      borderWidth: 2
    }
  },

  plugins: {
    // Optimize tooltips
    tooltip: {
      animation: false,
      mode: 'nearest',
      intersect: false
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Key Optimizations Explained

1. Selective Data Recording

Instead of recording every data point, we only store values when there's a significant change:

  • Reduces memory usage
  • Maintains visual accuracy
  • Improves processing performance

2. Efficient Updates

The debounced update pattern prevents excessive renders:

  • Groups multiple data updates into a single render
  • Reduces CPU usage
  • Maintains smooth animations

3. Data Cleanup

Implementing a point threshold system prevents memory issues:

  • Monitors total data points
  • Triggers cleanup when threshold is reached
  • Maintains consistent performance over time

4. Chart.js Optimizations

Several Chart.js-specific optimizations improve performance:

  • Disabled point rendering for smoother lines
  • Removed unnecessary animations
  • Optimized tooltip interactions
  • Reduced tick density
  • Simplified grid lines

Results

This implementation has several advantages over simpler approaches:

  • Memory Efficient: Only stores necessary data points
  • CPU Friendly: Minimizes renders and calculations
  • Smooth Updates: No visual jank during updates
  • Scale-Ready: Handles thousands of points efficiently
  • User Friendly: Maintains responsive interactions

Future Optimizations

While the current implementation is well-optimized for typical use cases, there are several potential future enhancements:

  1. Web Workers Integration

    • Offload data processing to a separate thread
    • Improve main thread performance for larger datasets
    • Enable more complex data transformations without UI impact
  2. Progressive Loading

    • Implement virtual scrolling for historical data
    • Load data chunks based on viewport
    • Improve initial load performance

Conclusion

Building an efficient real-time chart requires careful consideration of data management, render optimization, and user experience. While the implementation is more complex than basic examples, the benefits in performance and reliability make it worthwhile for production applications.
The key is finding the right balance between:

  • Update frequency vs. performance
  • Data accuracy vs. memory usage
  • Visual quality vs. render speed

This solution provides a solid foundation that can be adapted for various real-time visualization needs while maintaining excellent performance characteristics.

Any suggestions for further improvements would be appreciated 😀

Top comments (0)