Dashboard realtime adalah layar angka yang ikut berubah begitu datanya berubah, tanpa di-refresh. Di meja kasir, Bu Rini punya dashboard: jumlah order per status, total kilo hari ini, dan omzet. Bayangin papan skor di pertandingan sepak bola. Petugasnya gak mengecat ulang seluruh papan setiap ada operan; ia cuma mengganti angka yang berubah. Penonton yang datang di babak kedua membaca papan dulu (snapshot), lalu mengikuti gol berikutnya (delta), dan menit pertandingan memberi tahu apakah ada yang terlewat (nomor urut).
🧪 Tebakan cepat: Pagi ramai: 40 perubahan datang dalam 2 detik. Kalau dashboard menggambar ulang di setiap pesan, ia menggambar 40 kali. Kalau pesannya digabung dan digambar paling banyak sekali per 250 ms, berapa kali? Jawabannya ada di bagian "Gabung dulu, gambar sekali".
Angka di dashboard dihitung dari daftar order
Dashboard menyimpan daftar order, lalu semua angkanya dihitung dari daftar itu. Tarifnya Rp 7.000 per kilo.
const STATUS = ['Antre', 'Dicuci', 'Disetrika', 'Siap diambil']
function hitung(orders) {
const perStatus = Object.fromEntries(STATUS.map((s) => [s, 0]))
let kg = 0
for (const o of orders) {
perStatus[o.status]++
kg += o.kg
}
return { perStatus, kg, omzet: kg * 7000 }
}
| Order | Kg | Status |
|---|---|---|
| #12 Pak Darto | 3 | Dicuci |
| #13 Mbak Wulan | 2 | Disetrika |
| #14 Mas Yoga | 5 | Siap diambil |
| #15 Bu Tini | 4 | Antre |
Hasilnya: tiap status 1 order, 3 + 2 + 5 + 4 = 14 kg, omzet 14 × 7.000 = Rp 98.000. Angka dihitung ulang dari daftar, bukan ditambah-kurangi sendiri, jadi gak bisa melenceng.
Snapshot atau delta
Untuk grafik live, server yang mendorong data lewat SSE (cukup kalau satu arah) atau WebSocket, lalu frontend memperbarui grafiknya. Polling tiap detik membuang request walaupun gak ada yang berubah. Pertanyaannya: apa yang dikirim?
- Snapshot: seluruh keadaan. 4 order = 243 byte, 60 order = 3.372 byte, dan terus membesar.
- Delta: cuma yang berubah. Mas Eko memasukkan #15 ke mesin cuci:
{"seq":58,"id":15,"status":"Dicuci"}= 36 byte, berapa pun jumlah ordernya.
Di Lab, 40 perubahan pagi ramai (dari keadaan awal) = 1.856 byte sebagai delta, 21.387 byte sebagai snapshot. Delta membawa nilai baru (status: 'Dicuci'), bukan "+1 Dicuci, -1 Antre": delta yang sama diterima dua kali tetap menghasilkan angka yang benar.
// server realtime, namespace /staff
function ubahStatus(id, status) {
seq += 1
orders.get(id).status = status
staf.emit('papan:delta', { seq, id, status })
}
Gabung dulu, gambar sekali
Menggambar ulang itu mahal (layout, chart, animasi), dan mata gak bisa membaca 20 perubahan per detik. Jadi pesan masuk ditampung, lalu digambar paling banyak sekali per 250 ms:
let tampung = []
let timer = null
staf.on('papan:delta', (d) => {
tampung.push(d)
if (timer) return // sudah ada jadwal gambar
timer = setTimeout(() => {
timer = null
for (const x of tampung) terapkan(x)
tampung = []
gambar() // sekali untuk semua yang terkumpul
}, 250)
})
Diukur di Chromium 147 dengan 40 pesan acak dalam 2.000 ms: gambar di setiap pesan = 40 kali, digabung per 250 ms = 7 kali. Itu jawaban tebakannya. 200 pesan dalam 1 detik: 200 kali lawan 4 kali. Gak ada pesan yang hilang; semuanya diterapkan, cuma digambar bersamaan. Server juga bisa menggabung sebelum mengirim (satu pesan berisi beberapa delta per 250 ms), supaya jumlah pesannya ikut turun.
Yang buka belakangan: snapshot, lalu delta bernomor
Pukul 10.00 Bu Rini membuka dashboard di HP-nya. HP butuh keadaan sekarang (snapshot) lalu perubahan sesudahnya (delta). Urutannya penting:
| Waktu | Snapshot dulu, baru subscribe | Subscribe dulu, baru snapshot |
|---|---|---|
| 0 ms | GET /api/dashboard |
subscribe papan:delta, lalu GET /api/dashboard |
| 150 ms | Mas Eko: #15 Dicuci (seq 58), HP belum subscribe: terlewat | delta 58 ditampung |
| 300 ms | snapshot seq 57 tiba, baru subscribe | snapshot tiba (dibuat sesudah perubahan: seq 58), delta 58 dibuang karena sudah ada di dalamnya |
| 900 ms | delta seq 59 tiba: seq loncat dari 57 ke 59 | delta 59 diterapkan |
let orders = null // null: snapshot belum ada
let seqTerakhir = 0
let antre = []
function terapkan(d) { // { seq, id, status } atau { seq, baru: order }
if (d.baru) orders.push(d.baru)
else orders.find((o) => o.id === d.id).status = d.status
seqTerakhir = d.seq
}
staf.on('papan:delta', (d) => {
if (!orders) return antre.push(d) // snapshot belum ada: tampung
if (d.seq <= seqTerakhir) return // sudah termasuk di snapshot
if (d.seq !== seqTerakhir + 1) return muatUlang() // ada yang terlewat
terapkan(d)
gambar()
})
async function muatUlang() {
orders = null
const snap = await fetch('/api/dashboard').then((r) => r.json()) // { seq, orders }
orders = snap.orders
seqTerakhir = snap.seq
for (const d of antre.splice(0)) if (d.seq > seqTerakhir) terapkan(d)
gambar()
}
muatUlang() // dipanggil sesudah listener dipasang
- Subscribe lebih dulu, tampung, lalu ambil snapshot. Delta yang seq-nya ≤ seq snapshot dibuang.
- Kalau seq loncat (gap), jangan menebak: ambil snapshot lagi. Ini juga menyelamatkan HP yang sempat putus.
Grafik yang gak menggambar ulang semuanya
Delta #15 Antre ke Dicuci cuma mengubah dua batang. Simpan angka per status, dan biarkan batang yang angkanya sama gak ikut digambar ulang:
const Batang = memo(function Batang({ status, jumlah, maks }) {
return <div className="batang" style={{ width: `${(jumlah / maks) * 100}%` }}>{status}: {jumlah}</div>
})
Dengan memo, React melewati batang yang props-nya gak berubah. Library grafik juga punya cara serupa: ubah datanya, lalu minta grafik memperbarui diri sekali per batch, bukan membuat grafik baru. Kalau tab sedang tersembunyi (document.visibilityState === 'hidden'), lewati menggambar dan gambar sekali saat tab terlihat lagi.
⚠️ Jebakan yang sering ketemu
- Menggambar ulang di setiap pesan. 40 pesan, 40 kali render, layar patah-patah. Gabung per 250 ms.
- Mengirim snapshot di setiap perubahan. Ukurannya tumbuh bersama jumlah order. Kirim delta, snapshot cuma saat pertama buka atau saat ada gap.
- Ambil snapshot dulu baru subscribe. Perubahan di antara keduanya hilang selamanya. Subscribe dulu, tampung, lalu snapshot, dan bandingkan seq.
- Delta berbentuk "+1" dan "-1". Pesan dobel atau terlewat membuat angka melenceng. Kirim nilai barunya.
TL;DR: Dashboard Bu Rini menghitung angkanya (order per status, total kilo, omzet = kilo × Rp 7.000) dari daftar order. Server mendorong data lewat SSE atau WebSocket, berupa delta kecil bernomor ({"seq":58,"id":15,"status":"Dicuci"}, 36 byte) alih-alih snapshot yang membesar (40 perubahan: 1.856 lawan 21.387 byte). Tampung pesan dan gambar paling banyak sekali per 250 ms (40 pesan dalam 2 detik: 40 render jadi 7). Yang buka belakangan subscribe dulu, menampung delta, mengambil snapshot ber-seq, membuang delta yang seq-nya sudah termasuk, dan mengambil snapshot lagi kalau seq loncat. Dengan memo, cuma batang yang angkanya berubah yang digambar ulang.