Optimizing a Real-Time Windmill Monitoring Dashboard: Handling High-Frequency Data Streams in React
Introduction
One of the most interesting challenges I worked on involved building a real-time dashboard for monitoring wind turbine power generation.
The system received live telemetry every second from multiple windmills. The challenge wasn't receiving the data—it was displaying it efficiently without causing the React application to become sluggish or unresponsive.
This article explains the architecture, the performance bottleneck we encountered, and the optimization that significantly improved the application's responsiveness.
System Architecture
The data pipeline looked like this:
Windmill Hardware
│
▼
Java Adapter Service
│
▼
Socket.IO Server
│
▼
React Dashboard
│
▼
Power Generation GridComponents
- Windmill hardware continuously generated telemetry data.
- A Java adapter received raw hardware data and transformed it into a standard format.
- The Java service published processed data through Socket.IO.
- The React application subscribed to Socket.IO events and displayed the latest power-generation data in a grid.
The Challenge
Each wind turbine sent updated information every second, and the Socket.IO server immediately pushed every update to the React application.
Initially, every incoming message directly updated React state:
socket.on("power-update", (data) => {
setGridData(data);
});At first, this seemed like the correct approach. However, as the number of wind turbines increased, the application started experiencing:
- Frequent React re-renders
- High CPU usage
- UI lag
- Grid flickering
- Poor scrolling performance
- Reduced browser responsiveness
The problem became more noticeable when monitoring hundreds of turbines simultaneously.
Root Cause Analysis
Every Socket.IO event triggered:
- A React state update
- A component re-render
- Grid reconciliation
- DOM updates
When hundreds of updates arrived every second, React spent most of its time rendering instead of responding to user interactions. The issue wasn't Socket.IO—the issue was updating the UI as frequently as data arrived.
The Solution
Instead of updating React state for every incoming Socket.IO event, we decoupled data ingestion from UI rendering.
Step 1: Store Incoming Data in a Ref
We stored the latest telemetry in a useRef:
const latestDataRef = useRef([]);Whenever Socket.IO received new data:
socket.on("power-update", (data) => {
latestDataRef.current = data;
});Updating a ref does not trigger a React re-render. The application could now receive data continuously with minimal rendering overhead.
Step 2: Update the Grid at Fixed Intervals
Instead of rendering on every message, we refreshed the grid once every second:
useEffect(() => {
const timer = setInterval(() => {
setGridData(latestDataRef.current);
}, 1000);
return () => clearInterval(timer);
}, []);The new controlled flow became:
Socket.IO
│
▼
Update Ref
│
▼
No React Re-render
│
▼
Every 1 Second
│
▼
Update GridThe UI updated at a controlled rate while still displaying fresh data.
Why This Works
React state is designed to trigger UI updates. Refs store mutable values without causing re-renders. By separating data reception from UI rendering, the application could process every incoming message while limiting expensive rendering operations.
This pattern is especially useful in:
- Live dashboards
- IoT monitoring systems
- Financial trading platforms
- Industrial automation
- Manufacturing control systems
- Real-time analytics
Performance Improvements
After implementing this optimization, we observed:
- A significant reduction in unnecessary React renders
- Lower CPU utilization
- Smoother grid scrolling
- Improved browser responsiveness
- Stable performance with a large number of connected turbines
- Better scalability for future growth
Most importantly, users still saw near real-time data while the application remained responsive.
Lessons Learned
In real-time applications, receiving data quickly is only half the challenge. Displaying that data efficiently is equally important.
Updating React state for every incoming event may work for low-frequency updates, but high-frequency streams require a different approach. Using useRef as a temporary buffer and updating the UI on a controlled interval achieved a balance between real-time visibility and application performance.
Not every incoming event needs to trigger a UI render. Separating data ingestion from rendering is a simple yet powerful technique for building scalable real-time applications.
Conclusion
Real-time systems often fail not because of network throughput, but because the frontend tries to render faster than users can perceive changes. Introducing a lightweight buffering layer with useRef and rendering on a fixed interval allowed us to handle continuous telemetry efficiently while keeping the dashboard smooth and responsive.
This optimization became a key part of building a scalable monitoring solution capable of handling high-frequency data streams from industrial equipment.