Hydration mismatch pada dashboard AI dengan SSR bertahap terjadi ketika HTML yang dikirim server tidak identik dengan render awal JavaScript di browser. Akibatnya, UI bisa tampak seperti “diambil alih” ulang: badge status berubah sendiri, counter meloncat, panel bergeser, atau tombol action berdasarkan role tiba-tiba muncul/hilang setelah aplikasi aktif di client.
Masalah ini sering muncul pada dashboard yang menggabungkan SSR untuk first paint cepat, lalu menghidupkan bagian interaktif secara bertahap seperti panel insight, status job AI, data waktu nyata, dan kontrol berbasis otorisasi. Kuncinya bukan sekadar “memindahkan kode ke client”, melainkan memastikan state awal yang dipakai server dan browser benar-benar sinkron.
Mengapa hydration mismatch sering terjadi pada dashboard AI
Dashboard AI cenderung punya banyak sumber perubahan state sejak awal halaman dimuat:
- Panel status pipeline yang berubah dari queued, running, ke completed.
- Counter seperti jumlah token, job aktif, atau notifikasi.
- Data waktu nyata dari polling, SSE, atau WebSocket.
- Role-based action seperti tombol approve, retry, cancel, atau export yang tergantung session dan permission.
- Personalization berdasarkan locale, timezone, preferensi tema, atau feature flag.
Jika salah satu nilai di atas dihitung berbeda antara server dan browser pada render pertama, framework akan mendeteksi perbedaan markup. Dalam kasus ringan, akan muncul warning. Dalam kasus lebih serius, subtree tertentu dihapus dan dirender ulang, sehingga pengguna melihat UI berubah mendadak.
Gejala yang biasanya terlihat
1. Warning hydration di console
Contoh gejalanya: teks tidak cocok, atribut berbeda, atau struktur DOM berubah. Walau pesan spesifik bergantung framework, inti masalahnya sama: hasil SSR berbeda dari render awal client.
2. Komponen terlihat “mengambil alih” setelah load
Ini pola yang sering membingungkan. HTML awal terlihat benar, tetapi setelah JavaScript aktif:
- badge status berubah warna dan teks,
- counter dari 0 langsung menjadi 17,
- tombol admin muncul setelah session client terbaca,
- timestamp berubah format,
- panel yang tadinya ada menjadi kosong lalu terisi lagi.
3. Event handler terasa tidak stabil
Saat subtree harus dirender ulang karena mismatch, komponen interaktif bisa tampak berkedip, fokus input hilang, atau state lokal reset.
Root cause umum hydration mismatch
State awal tidak sinkron antara server dan browser
Ini penyebab paling umum. Server merender data A, tetapi browser saat boot memakai data B. Misalnya server mengirim jumlah job aktif = 12, sementara store client langsung mengisi 13 dari cache lokal atau request kedua yang selesai lebih cepat.
Prinsip aman: render pertama di browser harus menggunakan payload yang sama dengan SSR. Update baru boleh masuk setelah hydration selesai.
Waktu, timezone, dan locale
Nilai seperti new Date(), format lokal, atau relative time sering berbeda antara server dan user. Server mungkin berada di UTC, browser di Asia/Jakarta, atau locale default-nya berbeda. Akibatnya teks seperti “10:05” vs “17:05” atau “a few seconds ago” vs “baru saja” memicu mismatch.
Random value dan ID yang tidak stabil
Pemakaian Math.random(), timestamp saat render, atau generator ID yang dipanggil langsung di template bisa menghasilkan output berbeda antara server dan client. Ini juga berlaku untuk warna acak, key list, dan identifier elemen.
Akses window, document, atau localStorage saat render
Ketika render awal di browser membaca preferensi dari localStorage lalu mengubah isi UI, hasilnya bisa berbeda dari SSR. Contoh umum: tema, layout compact, panel yang dibuka terakhir, atau filter tersimpan.
Feature flag yang tidak konsisten
Jika server dan browser menghitung flag dari sumber yang berbeda, komponen tertentu bisa ada di satu sisi tetapi tidak di sisi lain. Pada dashboard AI, ini sering terjadi saat rollout panel eksperimen, model picker, atau tombol export beta.
Auth state dan role-based action
Server mungkin belum punya user context yang sama dengan browser, atau sebaliknya browser memuat session tambahan dari token lokal. Akibatnya tombol seperti Approve atau Retry Job muncul hanya di salah satu sisi.
Strategi debug hydration mismatch yang praktis
1. Cari komponen terkecil yang berbeda
Jangan mulai dari seluruh dashboard. Isolasi subtree yang paling sering berubah: badge status, counter, header user, atau timestamp. Bandingkan output SSR dan hasil render awal client pada komponen itu.
Tip: Jika mismatch terjadi pada halaman besar, nonaktifkan sementara panel real-time atau action berbasis role satu per satu untuk menemukan sumber perbedaan.
2. Log input render, bukan hanya output
Sering kali masalahnya bukan pada JSX/template, tetapi pada data masuk. Log nilai yang dipakai saat render awal:
- status job,
- locale/timezone,
- role user,
- feature flag aktif,
- counter awal dari payload SSR,
- nilai dari browser storage.
Tujuannya adalah menjawab: apakah server dan browser menerima input yang sama untuk render pertama?
3. Bekukan update real-time sampai hydration selesai
Polling, SSE, dan WebSocket jangan langsung mengubah state yang sama dengan output SSR sebelum hydration selesai. Jika tidak, browser bisa merender nilai baru lebih cepat daripada state SSR sempat dipakai secara konsisten.
4. Audit semua nilai yang tidak deterministik
Periksa penggunaan:
new Date()saat render,Math.random(),- ID yang dibuat inline,
- pembacaan cookie/storage hanya di client,
- format angka/waktu tanpa locale eksplisit.
5. Bandingkan HTML server dengan state bootstrap client
Pada pola SSR yang sehat, data yang membentuk HTML awal seharusnya juga tersedia sebagai payload bootstrap di browser. Jika client langsung membuat request ulang dan memakai hasil itu untuk render pertama, mismatch sangat mudah terjadi.
Pola aman untuk SSR bertahap
Gunakan snapshot state awal yang sama
Server sebaiknya merender dashboard dari satu snapshot data, lalu mengirim snapshot yang sama ke browser untuk dipakai pada render awal client. Setelah hydration selesai, barulah subscription real-time diaktifkan.
// pseudo-flow umum, berlaku lintas framework
const initialState = {
jobs: ssrJobs,
user: ssrUser,
flags: ssrFlags,
generatedAt: ssrGeneratedAt
}
renderServer(initialState)
bootstrapClient(initialState)
startRealtimeAfterHydration()Mengapa ini bekerja? Karena browser tidak menghitung ulang state awal dari sumber lain. Render pertama identik, lalu update terjadi sebagai perubahan state normal setelah aplikasi aktif.
Tunda elemen yang memang hanya valid di client
Beberapa UI memang lebih aman dirender setelah mount, misalnya:
- relative time lokal pengguna,
- preferensi dari
localStorage, - ukuran viewport,
- grafik yang bergantung API browser,
- panel eksperimen dari flag client-only.
Untuk bagian seperti ini, lebih baik tampilkan placeholder stabil saat SSR daripada menghasilkan markup berbeda.
Pisahkan data SSR dari update live
Jangan campur state “snapshot awal” dengan “stream update” pada render pertama. Simpan nilai SSR dulu, lalu merge patch real-time setelah hydration selesai. Ini sangat penting untuk counter, badge status, dan progress job AI.
Contoh implementasi ringkas
Next.js / React: jangan baca browser-only state saat render awal
import { useEffect, useState } from 'react'
export default function StatusPanel({ initialJob, initialRole }) {
const [job, setJob] = useState(initialJob)
const [role, setRole] = useState(initialRole)
const [hydrated, setHydrated] = useState(false)
useEffect(() => {
setHydrated(true)
// Aman dibaca setelah mount
const savedRole = window.localStorage.getItem('role')
if (savedRole) setRole(savedRole)
// Mulai realtime setelah hydration
const timer = setInterval(async () => {
const res = await fetch('/api/jobs/' + job.id)
const next = await res.json()
setJob(next)
}, 5000)
return () => clearInterval(timer)
}, [job.id])
return (
<section>
<span className={job.status}>{job.status}</span>
<strong>{job.activeTasks}</strong>
{hydrated && role === 'admin' ? <button>Retry Job</button> : null}
</section>
)
}Yang penting di sini bukan framework-nya, tetapi polanya: render awal memakai initialJob dan initialRole dari SSR, lalu hal yang bergantung browser dibaca setelah mount.
Nuxt / Vue: format waktu di client-only atau pakai nilai server yang sudah final
<template>
<div>
<span>{{ status }}</span>
<span v-if="mounted">{{ localTime }}</span>
<span v-else>{{ serverTimeLabel }}</span>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const props = defineProps({
status: String,
serverTimeLabel: String,
timestamp: String
})
const mounted = ref(false)
onMounted(() => {
mounted.value = true
})
const localTime = computed(() =>
new Date(props.timestamp).toLocaleString()
)
</script>Jika format waktu lokal pengguna penting, tampilkan label server yang stabil saat SSR lalu ganti setelah mount. Ini lebih aman daripada memformat waktu lokal di kedua sisi tanpa kontrol locale dan timezone yang sama.
SvelteKit: gunakan data load sebagai sumber tunggal render awal
<script>
import { onMount } from 'svelte'
export let data
let job = data.job
let canRetry = data.canRetry
onMount(() => {
const es = new EventSource(`/api/jobs/${job.id}/events`)
es.onmessage = (event) => {
const patch = JSON.parse(event.data)
job = { ...job, ...patch }
}
return () => es.close()
})
</script>
<span class={job.status}>{job.status}</span>
{#if canRetry}
<button>Retry Job</button>
{/if}Pola amannya: nilai awal berasal dari load dan dipakai apa adanya saat hydration. Stream event baru berjalan setelah komponen terpasang di browser.
Kesalahan umum yang sering tidak disadari
- Menggunakan key list yang berubah-ubah, misalnya dari random ID.
- Memformat angka dan tanggal tanpa locale eksplisit lalu berharap hasilnya sama di server dan browser.
- Melakukan fetch ulang terlalu cepat di client sehingga data awal langsung tertimpa sebelum hydration stabil.
- Menyembunyikan mismatch dengan client-only secara berlebihan. Ini memang menghindari warning, tetapi bisa mengorbankan manfaat SSR.
- Mencampur auth dari cookie server dan token localStorage tanpa strategi prioritas yang jelas.
- Menghitung feature flag dua kali dari sumber berbeda.
Checklist pencegahan sebelum deploy
- Pastikan render awal client memakai payload data yang sama dengan SSR.
- Jangan gunakan
Math.random(), timestamp dinamis, atau ID tidak stabil di markup awal. - Audit semua akses
window,document, danlocalStorageagar hanya dijalankan setelah mount bila memengaruhi UI. - Bekukan polling, SSE, atau WebSocket sampai hydration selesai.
- Samakan strategi auth dan role resolution antara server dan browser.
- Tetapkan locale/timezone eksplisit, atau tampilkan placeholder stabil saat SSR.
- Pastikan feature flag yang memengaruhi struktur UI tersedia konsisten di kedua sisi.
- Untuk panel client-only, gunakan fallback SSR yang tidak berubah struktur.
Kapan harus memakai client-only?
Client-only masuk akal jika komponen memang sangat bergantung pada API browser atau nilainya tidak mungkin disamakan saat SSR, misalnya editor kompleks, chart yang sangat interaktif, atau panel eksperimen personal. Tetapi untuk elemen inti dashboard seperti badge status, counter utama, dan action berbasis role, lebih baik pertahankan SSR dengan state awal yang sinkron agar first paint tetap berguna dan tidak menipu pengguna.
Penutup
Debug hydration mismatch pada dashboard AI dengan SSR bertahap pada dasarnya adalah pekerjaan menyamakan render input, bukan sekadar merapikan komponen. Jika server dan browser menggunakan snapshot state awal yang sama, lalu update real-time ditunda sampai hydration selesai, sebagian besar mismatch akan hilang. Fokuskan audit pada waktu/locale, random value, browser-only API, feature flag, dan auth state karena di situlah perbedaan paling sering muncul.
Jika UI terasa seperti “mengambil alih” setelah load, anggap itu sinyal bahwa SSR Anda belum menjadi sumber kebenaran untuk render pertama. Perbaiki sinkronisasi state awal, dan hydration akan kembali stabil.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!