This episode builds a live dashboard: high-frequency update needs, streaming and delta update patterns, Chart.js and D3 integration, and rendering optimization with throttling and Web Workers.

A monitoring dashboard is the most visible face of a real-time application: moving charts, changing numbers, appearing alerts. But a bad dashboard is a sin — a chart rendered 60 times per second freezes the browser, and late numbers are a lie.
Episode 22 covers live dashboard & data streaming: receiving high-frequency updates, choosing the right streaming pattern, integrating chart libraries, and keeping the browser smooth amid thousands of data points.
Not all dashboards are the same. First understand their characteristics:
Trading chart : puluhan update per detik
Monitoring : satu hingga beberapa per detik
Log viewer : burst, lalu hening
Analytics : satu per menit sudah cukupThe load determines the architecture: trading needs batching and sampling, a log viewer needs a queue that does not pile up. A dashboard that forces all data to all clients will collapse on itself.
There are two ways to send updates: a full snapshot or a delta.
const snapshot = {
type: "snapshot",
data: semuaPoin,
};
const delta = {
type: "delta",
data: { harga: 1023, pada: Date.now() },
};A snapshot is sent once when the client connects — it holds the full state. After that only delta: small changes that replace values. This pattern saves bandwidth many times over compared to sending the whole dataset on every update.
A large snapshot is split into several frames so it does not block the event loop and the socket.
function kirimChunk(socket, data) {
const ukuran = 1000;
for (let i = 0; i < data.length; i += ukuran) {
socket.emit("snapshot:chunk", data.slice(i, i + ukuran));
}
socket.emit("snapshot:selesai");
}data.slice(i, i + ukuran) sends 1000 points per frame. The client waits for the snapshot:selesai event, then renders all the data at once.
Chart.js fits dashboards that update a few times per second.
const chart = new Chart(ctx, {
type: "line",
data: { labels: [], datasets: [{ data: [] }] },
});
ws.onmessage = (event) => {
const d = JSON.parse(event.data);
chart.data.labels.push(d.label);
chart.data.datasets[0].data.push(d.nilai);
if (chart.data.labels.length > 60) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
chart.update();
};chart.data.datasets[0].data.push(d.nilai) adds a new point, shift() discards the oldest so only the last 60 points are rendered. This is an example of windowing: show a rolling window, not the whole history.
D3.js is more flexible for complex visualizations and large datasets. The key: do not hand D3 a full render on every update — use an update pattern that only moves the elements that changed.
The browser renders about 60 frames per second. Data updates arriving 200 times per second must be throttled.
let terakhir = 0;
ws.onmessage = (event) => {
const kini = performance.now();
if (kini - terakhir < 100) return;
terakhir = kini;
prosesUpdate(JSON.parse(event.data));
};kini - terakhir < 100 caps visual processing at 10 times per second. Data is still fully received, but rendering is kept at a speed that is comfortable for the eyes.
Heavy data aggregation and transformation should not run on the main thread.
const worker = new Worker("aggregator.js");
ws.onmessage = (event) => {
worker.postMessage(event.data);
};
worker.onmessage = (event) => {
renderChart(event.data);
};worker.postMessage(event.data) sends raw data to the worker, and the aggregation result returns to the main thread only for rendering. The main thread stays busy rendering the UI, not computing.
Episode 22 made a dashboard that is honest and smooth: the frugal delta pattern, chunked snapshots, windowing that protects memory, and throttling that protects the browser.
Key takeaways:
In the next episode we build multiplayer game basics: authoritative servers, game state synchronization, input handling, latency mitigation, and game-specific optimization.