Debug hydration mismatch pada UI skor SSR yang berubah-ubah berfokus pada satu masalah inti: markup yang dirender server tidak identik dengan render awal di browser. Pada komponen skor, ranking, atau hasil evaluasi, mismatch ini bukan sekadar warning teknis. Angka yang berubah setelah halaman tampil akan terlihat seperti sistem penilaian yang tidak konsisten, dan itu langsung merusak kepercayaan pengguna.
Analogi yang mudah dipahami adalah tren ATS atau resume scorer yang memberi nilai berbeda untuk dokumen yang tampak sama. Dari sudut pandang pengguna, perubahan skor tanpa penjelasan terasa seperti produk yang “asal hitung”. Dalam aplikasi SSR, gejala serupa sering muncul ketika server merender skor 82, lalu setelah hydration klien menampilkan 79 atau mengubah urutan ranking. Bagi engineer, ini biasanya berasal dari perbedaan sumber data, waktu render, locale, angka acak, state klien setelah mount, atau cache yang tidak sinkron.
Kenapa hydration mismatch pada UI skor sangat sensitif
SSR dipakai agar halaman bisa tampil cepat, mudah diindeks, dan punya state awal yang konsisten. Namun pada UI skor, konsistensi visual jauh lebih penting daripada pada elemen dekoratif. Ketika angka, label, atau ranking berubah tepat setelah load, pengguna akan mempertanyakan validitas sistem, bukan hanya kualitas frontend.
Contoh dampak yang umum:
- Skor berubah dari 84 ke 81 setelah hydration.
- Urutan ranking bergeser karena klien melakukan sort dengan aturan berbeda.
- Format angka berubah dari
1.234,5ke1,234.5, lalu parser internal menghasilkan nilai lain. - Badge atau status seperti “Lulus threshold” muncul di server tetapi hilang di klien.
- Warning hydration di console, tetapi tim mengabaikannya karena UI “masih jalan”.
Pada produk yang menampilkan evaluasi, trust adalah bagian dari fitur. Jadi solusi bukan hanya “menghilangkan warning”, tetapi memastikan hasil SSR dan hydration stabil, dapat dijelaskan, dan dapat direproduksi.
Gejala yang perlu dicurigai
1. Warning hydration di console
Framework biasanya memberi indikasi seperti teks mismatch, node mismatch, atau perbedaan atribut. Jangan berhenti di warning-nya; warning hanya gejala dari ketidakselarasan data atau logika render.
2. Nilai skor berubah tepat setelah halaman interaktif
Ini sering terjadi ketika server merender data A, lalu browser langsung fetch data B yang lebih baru, berbeda locale, atau diproses ulang dengan rumus berbeda.
3. UI berkedip atau re-order daftar
Jika ranking berubah urutan saat mount, biasanya ada sort berbasis data yang tidak deterministik, perbedaan precision angka, atau array yang diubah ulang di klien.
4. Bug hanya muncul di produksi
Lingkungan produksi lebih sering memunculkan mismatch karena ada CDN cache, region server berbeda, timezone berbeda, edge runtime berbeda, atau race condition saat data di-refresh.
Akar masalah paling umum
Perbedaan data fetch antara server dan klien
Kasus paling umum: server merender snapshot data tertentu, tetapi setelah mount klien melakukan fetch ulang ke endpoint yang bisa menghasilkan skor berbeda. Jika skor bersifat dinamis atau dihitung dari service lain, mismatch sangat mungkin terjadi.
Pola masalah:
- Server mengambil data dari database read replica, klien dari API gateway yang lebih baru.
- Server menerima cookie/header tertentu, klien tidak.
- Klien melakukan refetch otomatis tanpa menunggu state awal dipakai.
- Endpoint mengembalikan nilai yang berubah karena model skoring atau metadata baru.
Perbedaan waktu render
Jika skor bergantung pada waktu saat ini, countdown, expiry, “last updated”, atau threshold berdasarkan tanggal, maka server dan klien bisa merender hasil berbeda hanya karena jeda beberapa ratus milidetik atau beda timezone.
Locale dan format angka
Formatting adalah sumber bug yang sering diremehkan. Jika server memformat angka menggunakan locale tertentu tetapi klien menggunakan locale browser, string hasil render bisa berbeda. Lebih buruk lagi, jika string terformat itu dipakai ulang untuk parsing atau sort, hasil ranking juga bisa berubah.
Random value atau non-deterministic computation
Pemakaian Math.random(), UUID saat render, urutan object yang tidak stabil, atau algoritma scoring yang bergantung pada source non-deterministic akan membuat output server dan klien tidak identik.
State klien berubah setelah mount
Hydration mismatch juga sering berasal dari logika seperti: baca localStorage, cek fitur eksperimen, ambil preferensi user, atau hitung ulang skor berdasarkan data browser. Jika semua ini mempengaruhi render awal, hasilnya akan berbeda dari SSR.
Cache tidak sinkron
Server, browser, data fetching layer, dan CDN bisa memiliki snapshot berbeda. Pada halaman skor, ini berbahaya karena pengguna melihat “kebenaran” yang berubah-ubah. Cache stale bukan hanya masalah performa; pada kasus ini ia menjadi masalah konsistensi.
Pola salah vs benar
Pola salah: hitung skor langsung saat render dengan nilai non-deterministic
// Salah: waktu saat ini mempengaruhi render awal SSR dan client render awal
function ScoreCard({ baseScore }) {
const score = baseScore + (Date.now() % 3);
return <div>Skor: {score}</div>;
}Kenapa salah: server dan klien hampir pasti punya nilai waktu berbeda, sehingga teks yang dirender tidak sama.
Pola benar: hitung skor deterministik dari data yang sama
// Benar: render awal hanya memakai data yang sudah diputuskan di server
function ScoreCard({ score }) {
return <div>Skor: {score}</div>;
}Jika skor memang harus berubah real-time, tampilkan pembaruan setelah hydration dengan state yang jelas, misalnya label “diperbarui” atau timestamp.
Pola salah: format angka mengikuti locale browser saat render awal
// Salah: locale bisa berbeda antara server dan browser
function ScoreCard({ score }) {
return <div>{score.toLocaleString()}</div>;
}Jika lingkungan server dan browser memakai locale berbeda, string hasil render bisa mismatch.
Pola benar: tetapkan locale eksplisit
// Lebih aman: locale dan opsi format eksplisit
function formatScore(score) {
return new Intl.NumberFormat('id-ID', {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
}).format(score);
}
function ScoreCard({ score }) {
return <div>{formatScore(score)}</div>;
}Pola salah: refetch otomatis mengganti state awal tanpa kontrol
// Pseudocode
const initialScore = props.score;
const [score, setScore] = useState(initialScore);
useEffect(() => {
fetch('/api/score').then(r => r.json()).then(data => setScore(data.score));
}, []);Masalahnya bukan sekadar fetch, tetapi perubahan hasil tanpa konteks. Pengguna melihat SSR 82, lalu langsung berubah menjadi 79 tanpa penjelasan.
Pola benar: bedakan snapshot SSR dan pembaruan setelah mount
// Pseudocode
const [score, setScore] = useState(initialScore);
const [updatedAt, setUpdatedAt] = useState(initialUpdatedAt);
const [isRefreshing, setIsRefreshing] = useState(false);
useEffect(() => {
let active = true;
setIsRefreshing(true);
fetch('/api/score')
.then(r => r.json())
.then(data => {
if (!active) return;
if (data.updatedAt !== updatedAt) {
setScore(data.score);
setUpdatedAt(data.updatedAt);
}
})
.finally(() => {
if (active) setIsRefreshing(false);
});
return () => {
active = false;
};
}, [updatedAt]);Dengan pola ini, Anda bisa menampilkan status “memeriksa pembaruan” atau “skor diperbarui”, sehingga perubahan dapat dijelaskan, bukan tampak acak.
Langkah investigasi yang efektif
1. Pastikan apa yang berbeda: data, markup, atau formatting
Jangan langsung berasumsi masalah ada di framework. Bandingkan tiga hal berikut:
- Payload data SSR yang dipakai saat server merender.
- State awal klien sebelum efek berjalan.
- Data hasil refetch setelah mount.
Sering kali mismatch bukan karena hydration engine, tetapi karena data yang dipakai pada render pertama memang berbeda.
2. Log nilai mentah, bukan hanya hasil format
Jika Anda hanya melihat “82,5” vs “82.5”, bug bisa tampak kecil padahal parser atau sorter membaca keduanya berbeda. Log nilai numerik asli, locale, timezone, dan timestamp yang dipakai saat render.
// Pseudocode untuk debugging
console.log({
rawScore,
formattedScore,
locale,
timezone,
generatedAt,
});3. Bekukan input render
Untuk sementara, matikan refetch klien, random value, waktu dinamis, dan eksperimen berbasis browser. Jika mismatch hilang, Anda sudah mempersempit area masalah ke sumber non-deterministic.
4. Bandingkan HTML server dengan render awal klien
Lihat source HTML yang dikirim server dan bandingkan dengan output setelah aplikasi aktif. Fokus pada node yang memuat skor, ranking, label threshold, dan formatting angka. Ini lebih bernilai daripada hanya membaca stack trace warning.
5. Audit semua kode yang berjalan saat render awal
Cari penggunaan:
Date.now(),new Date()Math.random()toLocaleString()tanpa locale eksplisitwindow,localStorage, atau data browser lain- sort berbasis comparator yang tidak stabil
- mutasi array/object saat render
6. Periksa jalur cache end-to-end
Tanyakan:
- Apakah HTML SSR di-cache lebih lama daripada API skor?
- Apakah browser melakukan revalidate dan mendapat data baru seketika?
- Apakah CDN, edge cache, dan server app punya aturan cache berbeda?
- Apakah key cache mengabaikan user, locale, atau parameter penting?
Hydration mismatch sering hanya gejala dari strategi cache yang tidak konsisten.
Perbaikan praktis di Next.js, Nuxt.js, dan pendekatan umum
Prinsip utama: render awal harus deterministik
Baik di Next.js maupun Nuxt.js, prinsipnya sama: data dan logika yang mempengaruhi HTML awal harus menghasilkan output yang sama di server dan pada render pertama klien.
Artinya:
- Gunakan snapshot data yang sama untuk SSR dan state awal klien.
- Jangan hitung ulang skor di render awal berdasarkan sumber yang hanya tersedia di browser.
- Tetapkan locale, timezone, dan aturan formatting secara eksplisit.
- Pindahkan pembaruan non-kritis ke fase setelah mount, dengan UI yang menjelaskan perubahan.
Di Next.js
Pada Next.js, perhatian utamanya adalah memastikan data yang dipakai di server juga menjadi sumber state awal komponen klien. Hindari refetch tanpa alasan tepat setelah mount jika hasilnya bisa mengubah angka yang baru saja dilihat pengguna. Jika perlu refetch, beri indikator bahwa data sedang diverifikasi atau diperbarui.
Untuk komponen yang benar-benar bergantung pada browser, pertimbangkan render khusus klien. Tetapi ini adalah trade-off: Anda menghindari mismatch dengan mengorbankan SSR pada bagian tersebut.
Di Nuxt.js
Pada Nuxt.js, masalah yang sama muncul jika data async di server tidak identik dengan state hidrasi di klien, atau jika ada logika browser-only yang ikut menentukan output awal. Pisahkan dengan tegas mana data snapshot SSR, mana data pembaruan klien, dan mana presentasi yang bergantung pada lingkungan browser.
Framework-agnostic: pisahkan snapshot, presentasi, dan live update
Pola arsitektur yang paling aman untuk UI skor adalah:
- Snapshot SSR: nilai skor, ranking, alasan utama, timestamp, locale yang dipakai.
- Presentasi deterministik: formatting hanya dari snapshot itu.
- Live update setelah mount: optional, diberi indikator perubahan.
Dengan pola ini, SSR menjadi sumber kebenaran awal yang bisa dijelaskan, sedangkan pembaruan klien menjadi event terpisah, bukan perubahan misterius.
Kasus spesifik yang sering menipu
Sort ranking yang tampak sama tetapi hasilnya berbeda
Jika beberapa item punya skor identik, urutan bisa berbeda bila comparator tidak lengkap. Tambahkan tie-breaker deterministik, misalnya ID atau nama yang sudah dinormalisasi.
// Lebih aman: sort deterministik
items.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
return String(a.id).localeCompare(String(b.id));
});Formatting dipakai sebagai sumber data
Jangan pernah melakukan sort, compare, atau threshold check berdasarkan string hasil format. Simpan angka mentah untuk logika, gunakan string hanya untuk tampilan.
Timezone server berbeda dengan user
Jika label seperti “diperbarui 1 menit lalu” dirender di server, kemungkinan besar akan berbeda saat klien menghitung ulang. Untuk informasi relatif terhadap waktu, lebih aman render placeholder stabil terlebih dahulu atau render timestamp absolut yang konsisten.
Eksperimen atau personalisasi terlalu dini
Jika pengguna A dan B menerima variasi skor atau label dari feature flag yang baru diketahui di klien, hasil SSR bisa berbeda dari hydration. Putuskan variasi eksperimen di server bila itu mempengaruhi HTML awal.
Checklist debugging hydration mismatch
- Apakah skor SSR dan state awal klien berasal dari snapshot data yang sama?
- Apakah ada refetch otomatis segera setelah mount?
- Apakah ada
Date.now(),new Date(), atau waktu relatif di render awal? - Apakah ada
Math.random(), UUID, atau nilai non-deterministic lain? - Apakah locale dan timezone ditetapkan eksplisit?
- Apakah angka mentah dipisahkan dari string tampilan?
- Apakah urutan ranking punya tie-breaker deterministik?
- Apakah ada pembacaan
window,localStorage, atau state browser pada render awal? - Apakah HTML SSR, API, dan cache CDN punya freshness policy yang konsisten?
- Apakah perubahan setelah mount diberi indikator yang bisa dipahami pengguna?
Strategi pencegahan agar UI skor tetap stabil
1. Jadikan snapshot server sebagai kontrak
Server sebaiknya mengirim bukan hanya nilai skor, tetapi juga metadata yang menjelaskan konteksnya: versi model atau rule set internal, timestamp, locale, dan alasan utama jika perlu. Ini memudahkan audit dan debugging ketika pengguna mempertanyakan hasil.
2. Pisahkan “initial truth” dari “latest truth”
SSR menampilkan initial truth. Jika ada pembaruan data setelah mount, tampilkan sebagai latest truth dengan affordance UI yang jelas, misalnya “Skor diperbarui berdasarkan data terbaru”. Pengguna akan menerima perubahan jika perubahan itu dijelaskan.
3. Hindari logika bisnis penting di layer presentasi
Semakin banyak skor dihitung ulang di komponen UI, semakin besar peluang mismatch. Lebih aman jika skor final sudah diputuskan di backend atau di layer data yang sama untuk server dan klien.
4. Uji SSR dan hydration sebagai satu sistem
Jangan hanya menguji komponen secara isolated. Tambahkan pengujian end-to-end yang memeriksa bahwa angka awal yang terlihat pengguna tetap sama setelah halaman interaktif, kecuali memang ada event pembaruan yang dirancang.
5. Definisikan kebijakan cache untuk data sensitif
Untuk halaman skor atau ranking, konsistensi sering lebih penting daripada agresivitas cache. Jika HTML SSR di-cache lama tetapi API real-time sangat segar, mismatch akan menjadi pola permanen. Pilih strategi cache yang membuat snapshot awal dan refresh klien tetap koheren.
Catatan praktis: jika sebuah bagian UI memang tidak bisa dibuat deterministik saat SSR, lebih baik akui dan render bagian itu khusus di klien daripada memaksa SSR lalu menampilkan angka yang berubah tanpa penjelasan.
Penutup
Hydration mismatch pada UI skor SSR yang berubah-ubah hampir selalu berakar pada satu hal: render awal tidak deterministik. Pada produk yang menampilkan skor, ranking, atau hasil evaluasi, ini bukan sekadar masalah konsol, tetapi masalah kepercayaan pengguna. Seperti pada produk ATS yang skornya terasa berubah tanpa alasan, pengguna akan menganggap sistem tidak dapat dipercaya jika angka bergeser tepat setelah halaman dimuat.
Solusi yang efektif adalah memastikan snapshot SSR menjadi dasar render awal yang stabil, lalu memisahkan pembaruan klien sebagai langkah terpisah yang eksplisit. Mulailah dari data fetch, waktu render, locale, random value, state setelah mount, dan cache. Jika keenam area ini diaudit dengan disiplin, sebagian besar kasus hydration mismatch bisa ditemukan dan diperbaiki sebelum sampai ke pengguna.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!