Vai al contenuto principale
Torna al blog
IoT Dashboard WebSocket React

Come costruire un dashboard IoT in tempo reale

Tutorial passo dopo passo per costruire un dashboard IoT in tempo reale. Stack tecnico, WebSocket, serie temporali, React, alert e architettura dei dati.

JM
Javier Manzano
CEO & Co-founder • 12 giugno 2026

Un dashboard IoT in tempo reale non è semplicemente una pagina web con grafici. È un sistema completo che coinvolge ingestione dei dati, elaborazione, archiviazione, trasmissione e rendering, tutto ottimizzato affinché i dati arrivino dal sensore all’occhio dell’utente in meno di un secondo.

In questa guida ti spiego come costruirne uno da zero, basandomi sull’esperienza reale di averli costruiti per clienti come Spherag (monitoraggio agricolo 24/7), InfoAdex (55M+ record pubblicitari) e Orquest (gestione della forza lavoro).

Requisiti di un dashboard IoT reale

Prima di scegliere le tecnologie, definisci di cosa ha bisogno il tuo dashboard:

Funzionali

  • Mostrare dati aggiornati in tempo reale (< 1 secondo di latenza)
  • Visualizzare serie temporali con zoom e navigazione
  • Alert visivi quando un valore esce dal range
  • Più fonti di dati in una vista unificata
  • Filtri per dispositivo, posizione, intervallo temporale
  • Esportazione dati (CSV, PDF)

Non funzionali

  • Funzionare 24/7 senza degrado delle prestazioni
  • Supportare centinaia di widget che si aggiornano simultaneamente
  • Renderizzare migliaia di punti dati senza bloccare la UI
  • Riconnessione automatica se si perde la connessione
  • Responsive: funzionare su schermi di controllo e mobile
  • Tempo di caricamento iniziale < 3 secondi

Stack tecnico consigliato

Dopo aver iterato su molteplici progetti, questo è lo stack che consigliamo per un dashboard IoT in tempo reale:

Frontend

TecnologiaPer cosa
React 18+Framework UI con Concurrent Features
Recharts o VisxGrafici di serie temporali performanti
D3.jsVisualizzazioni custom complesse
TanStack QueryGestione stato server + cache
Socket.IO o WebSocket nativoDati in tempo reale
Tailwind CSSStili rapidi e coerenti
ZustandStato globale leggero

Backend

TecnologiaPer cosa
Node.js + FastifyAPI REST + WebSocket server
RedisCache + pub/sub per broadcast
TimescaleDBDatabase di serie temporali
PostgreSQLMetadati, configurazioni, utenti
Broker MQTT (EMQX/Mosquitto)Ricezione dati dai dispositivi

Infrastruttura

TecnologiaPer cosa
Docker + Docker ComposeSviluppo locale e deploy
KubernetesProduzione scalabile
NginxReverse proxy + WebSocket upgrade
Prometheus + GrafanaMonitoraggio del dashboard stesso

Architettura dei dati

Flusso dati end-to-end

Dispositivo IoT
    ↓ (MQTT)
Broker MQTT
    ↓ (sottoscrizione)
Servizio di ingestione (Node.js)
    ↓                    ↓
TimescaleDB          Redis pub/sub
(archiviazione)      (tempo reale)
    ↓                    ↓
API REST             WebSocket server
    ↓                    ↓
Dashboard (dati storici + dati in tempo reale)

Separare i dati storici dai dati in tempo reale

Questo è il pattern chiave. Il dashboard ha bisogno di due flussi di dati diversi:

  1. Dati storici: Quando l’utente carica la pagina o cambia l’intervallo temporale, vengono interrogati i dati archiviati in TimescaleDB via API REST
  2. Dati in tempo reale: Una volta caricata la vista, i nuovi dati arrivano via WebSocket e vengono aggiunti al grafico senza ricaricamento

Questo permette al dashboard di mostrare mesi di storico con zoom efficiente E aggiornarsi in tempo reale con i nuovi dati.

Passo 1: Ingestione dati MQTT

Il primo componente è un servizio che si iscrive al broker MQTT e processa i messaggi in arrivo.

// ingestion-service.js (Node.js)
import mqtt from 'mqtt';
import { pool } from './db.js';
import { redis } from './redis.js';

const client = mqtt.connect(process.env.MQTT_BROKER_URL, {
  username: process.env.MQTT_USER,
  password: process.env.MQTT_PASS,
  clean: false,
  clientId: 'dashboard-ingestion-01'
});

client.subscribe('devices/+/telemetry', { qos: 1 });

client.on('message', async (topic, payload) => {
  const deviceId = topic.split('/')[1];
  const data = JSON.parse(payload.toString());

  // 1. Archiviare in TimescaleDB
  await pool.query(
    `INSERT INTO telemetry (device_id, timestamp, temperature, humidity, battery)
     VALUES ($1, NOW(), $2, $3, $4)`,
    [deviceId, data.temperature, data.humidity, data.battery]
  );

  // 2. Pubblicare su Redis per broadcast WebSocket
  await redis.publish('realtime:telemetry', JSON.stringify({
    deviceId,
    timestamp: Date.now(),
    ...data
  }));

  // 3. Valutare le regole di alert
  await evaluateAlerts(deviceId, data);
});

Questo servizio fa tre cose con ogni messaggio: lo archivia per le query storiche, lo pubblica su Redis affinché il WebSocket server lo trasmetta ai dashboard connessi, e valuta se deve attivare un alert.

Passo 2: WebSocket server

Il WebSocket server mantiene connessioni persistenti con i dashboard e invia loro i nuovi dati in tempo reale.

// websocket-server.js
import { WebSocketServer } from 'ws';
import { redis } from './redis.js';

const wss = new WebSocketServer({ port: 8080 });
const subscriber = redis.duplicate();

// Iscriversi al canale Redis
await subscriber.subscribe('realtime:telemetry');

subscriber.on('message', (channel, message) => {
  const data = JSON.parse(message);

  // Broadcast a tutti i client connessi
  // (in produzione, filtrare per le sottoscrizioni di ogni client)
  wss.clients.forEach(client => {
    if (client.readyState === 1) {
      client.send(message);
    }
  });
});

wss.on('connection', (ws, req) => {
  // Autenticazione
  const token = new URL(req.url, 'http://localhost').searchParams.get('token');
  if (!verifyToken(token)) {
    ws.close(4001, 'Unauthorized');
    return;
  }

  // Heartbeat per rilevare connessioni morte
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
});

// Ping periodico per rilevare le disconnessioni
setInterval(() => {
  wss.clients.forEach(ws => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

WebSocket vs Server-Sent Events (SSE)

  • WebSocket: Bidirezionale. Usalo se il dashboard invia comandi al server (filtri dinamici, controllo dispositivi)
  • SSE: Unidirezionale server-client. Più semplice, funziona meglio con proxy HTTP. Usalo se il dashboard riceve solo dati

Per la maggior parte dei dashboard IoT, SSE è sufficiente e più semplice da implementare. WebSocket è necessario quando c’è frequente interazione bidirezionale.

Passo 3: API REST per dati storici

Il dashboard ha bisogno di un’API per caricare i dati storici quando l’utente cambia l’intervallo temporale o applica filtri.

// api.js (Fastify)
app.get('/api/telemetry/:deviceId', async (req, reply) => {
  const { deviceId } = req.params;
  const { from, to, interval } = req.query;

  // Downsampling automatico secondo l'intervallo temporale
  const bucket = calculateBucket(from, to);

  const result = await pool.query(`
    SELECT
      time_bucket($1, timestamp) AS time,
      AVG(temperature) as temperature,
      AVG(humidity) as humidity,
      MIN(battery) as battery
    FROM telemetry
    WHERE device_id = $2
      AND timestamp BETWEEN $3 AND $4
    GROUP BY time
    ORDER BY time ASC
  `, [bucket, deviceId, from, to]);

  return result.rows;
});

function calculateBucket(from, to) {
  const diffHours = (new Date(to) - new Date(from)) / 3600000;
  if (diffHours <= 1) return '1 minute';
  if (diffHours <= 24) return '5 minutes';
  if (diffHours <= 168) return '1 hour';
  if (diffHours <= 720) return '6 hours';
  return '1 day';
}

Il trucco del downsampling è fondamentale: quando l’utente vede un anno di dati, non ha bisogno di 525.600 punti (uno al minuto). Ha bisogno di ~365 punti (uno al giorno). TimescaleDB ha time_bucket nativo che fa questo in modo efficiente.

Passo 4: Frontend React

Hook per dati in tempo reale

// useRealtimeData.js
import { useEffect, useCallback, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';

export function useRealtimeData(deviceId) {
  const queryClient = useQueryClient();
  const wsRef = useRef(null);

  useEffect(() => {
    const ws = new WebSocket(
      `${WS_URL}?token=${getToken()}&device=${deviceId}`
    );

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);

      // Aggiornare la cache di TanStack Query
      queryClient.setQueryData(
        ['telemetry', deviceId],
        (old) => old ? [...old.slice(-999), data] : [data]
      );
    };

    ws.onclose = () => {
      // Riconnessione automatica con backoff esponenziale
      setTimeout(() => reconnect(), getBackoff());
    };

    wsRef.current = ws;
    return () => ws.close();
  }, [deviceId]);
}

Componente grafico con dati in tempo reale

// TimeSeriesChart.jsx
import { useQuery } from '@tanstack/react-query';
import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts';
import { useRealtimeData } from './useRealtimeData';

function TimeSeriesChart({ deviceId, metric, from, to }) {
  // Dati storici
  const { data: historicalData } = useQuery({
    queryKey: ['telemetry', deviceId, from, to],
    queryFn: () => fetchTelemetry(deviceId, from, to),
  });

  // Dati in tempo reale (vengono aggiunti alla fine)
  useRealtimeData(deviceId);

  return (
    <LineChart data={historicalData} width={800} height={300}>
      <XAxis dataKey="time" />
      <YAxis />
      <Tooltip />
      <Line
        type="monotone"
        dataKey={metric}
        stroke="#5dd3b3"
        dot={false}
        isAnimationActive={false}
      />
    </LineChart>
  );
}

Nota isAnimationActive={false}: nei dashboard con aggiornamenti frequenti, le animazioni di transizione bloccano il render e degradano le prestazioni.

Passo 5: Sistema di alert

// alerts.js
async function evaluateAlerts(deviceId, data) {
  const rules = await getAlertRules(deviceId);

  for (const rule of rules) {
    const value = data[rule.metric];
    let triggered = false;

    switch (rule.condition) {
      case 'above': triggered = value > rule.threshold; break;
      case 'below': triggered = value < rule.threshold; break;
      case 'equals': triggered = value === rule.threshold; break;
    }

    if (triggered) {
      await createAlert({
        deviceId,
        ruleId: rule.id,
        value,
        threshold: rule.threshold,
        severity: rule.severity,
        timestamp: Date.now()
      });

      // Notificare via i canali configurati
      await notify(rule.channels, {
        device: deviceId,
        metric: rule.metric,
        value,
        threshold: rule.threshold
      });
    }
  }
}

Ottimizzazioni di performance

1. Throttling degli aggiornamenti

Se un dispositivo invia dati ogni 100ms, non hai bisogno di aggiornare il dashboard 10 volte al secondo. Raggruppa gli aggiornamenti:

// Nel WebSocket server
const buffer = new Map();

subscriber.on('message', (channel, message) => {
  const data = JSON.parse(message);
  buffer.set(data.deviceId, data);
});

// Flush ogni 200ms
setInterval(() => {
  if (buffer.size > 0) {
    const batch = Array.from(buffer.values());
    broadcast(JSON.stringify({ type: 'batch', data: batch }));
    buffer.clear();
  }
}, 200);

2. Virtualizzazione delle liste

Se hai 1.000 dispositivi, non renderizzare 1.000 righe. Usa la virtualizzazione (react-window o react-virtual) per renderizzare solo quelle visibili.

3. Web Workers per elaborazione pesante

Se devi calcolare statistiche o rilevare anomalie nel frontend, fallo in un Web Worker per non bloccare il thread principale.

4. Canvas invece di SVG per molti punti

Recharts usa SVG di default, che si degrada con più di 5.000 punti. Per serie temporali ad alta risoluzione, usa una libreria basata su Canvas (come uPlot o lightweight-charts).

Deploy e operazioni

Docker Compose per lo sviluppo

services:
  mqtt:
    image: emqx/emqx:latest
    ports: ["1883:1883", "8083:8083"]

  timescaledb:
    image: timescale/timescaledb:latest-pg16
    environment:
      POSTGRES_PASSWORD: dev
    ports: ["5432:5432"]

  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]

  ingestion:
    build: ./services/ingestion
    depends_on: [mqtt, timescaledb, redis]

  api:
    build: ./services/api
    ports: ["3000:3000"]
    depends_on: [timescaledb, redis]

  dashboard:
    build: ./frontend
    ports: ["5173:5173"]

Monitoraggio del dashboard stesso

Usa Prometheus per monitorare:

  • Numero di connessioni WebSocket attive
  • Latenza di scrittura in TimescaleDB
  • Messaggi MQTT processati al secondo
  • Tempo di risposta dell’API
  • Uso della memoria del WebSocket server

Alternative: Grafana vs Custom

Usa Grafana se:

  • Il dashboard è per uso interno tecnico
  • TimescaleDB/InfluxDB è la tua fonte di dati principale
  • Non hai bisogno di UX personalizzata
  • Vuoi essere operativo in giorni, non settimane

Costruisci custom se:

  • Il dashboard è per utenti finali
  • Hai bisogno di branding e UX specifica
  • Hai interazioni complesse (comandi a dispositivi, workflow)
  • Hai bisogno di funzionalità non supportate da Grafana

In molti progetti, la soluzione migliore è entrambe: Grafana per il team ops e un dashboard custom per gli utenti di business.

Conclusione

Costruire un dashboard IoT in tempo reale non è banale, ma con lo stack corretto e i pattern adeguati (separazione storico/tempo reale, downsampling, throttling), puoi avere un sistema robusto e performante.

I punti chiave:

  1. Separa i dati storici (API REST + TimescaleDB) dai dati in tempo reale (WebSocket + Redis pub/sub)
  2. Implementa il downsampling automatico secondo l’intervallo temporale
  3. Usa il throttling per non saturare il frontend con aggiornamenti
  4. Progetta il sistema di alert come componente indipendente
  5. Monitora il dashboard stesso come qualsiasi sistema in produzione

Se hai bisogno di aiuto per costruire il tuo dashboard in tempo reale o la tua piattaforma IoT industriale, in Soamee lo facciamo da anni. Prenota una consulenza gratuita e vediamo il tuo caso.

Non perderti nulla

JM

Javier Manzano

CEO & Co-founder in Soamee

Appassionato di tecnologia e sviluppo software. Condividendo conoscenze e esperienze per aiutare altri sviluppatori a crescere.

Ti è piaciuto questo articolo?

Se hai bisogno di aiuto con il tuo progetto di sviluppo, siamo qui per te.

Prenota una call gratuita →