Hydration mismatch di SSR terjadi saat HTML hasil render di server tidak cocok dengan hasil render awal di browser. Gejalanya bisa berupa warning di console, UI yang “meloncat” setelah mount, event handler yang tidak menempel seperti yang diharapkan, atau komponen yang diam-diam dirender ulang di client.
Masalah ini sering terlihat seperti bug frontend biasa, padahal akar persoalannya lebih dekat ke persoalan state dan konfigurasi: server membuat satu versi realitas, client membuat versi lain. Sudut pandang ini mirip pelajaran penting dari dunia Kubernetes dan sistem terdistribusi: kalau model mental tentang desired state, actual state, dan sumber kebenaran tidak sinkron, sistem akan berperilaku mengejutkan. Dalam konteks SSR, hydration mismatch biasanya muncul karena asumsi server dan browser tidak sama saat render pertama.
Catatan: anggap hydration sebagai proses “menyambungkan” HTML statis dari server dengan aplikasi interaktif di browser. Agar proses ini aman, output render awal di client harus semirip mungkin dengan output server.
Mengapa hydration mismatch sering terjadi pada aplikasi SSR modern?
Pada aplikasi SSR, ada minimal dua lingkungan eksekusi yang ikut menentukan UI:
- Server, yang merender HTML awal.
- Browser, yang menjalankan JavaScript, memulihkan state, lalu mengaktifkan interaksi.
Jika keduanya memakai input yang berbeda, maka hasil render bisa berbeda. Ini bukan sekadar soal framework, tetapi soal kontrak data dan state. HTML awal seharusnya dibangun dari data yang sama dengan yang dipakai client saat hydration.
Secara praktis, mismatch sering lahir dari pertanyaan-pertanyaan ini:
- Apakah server dan client membaca timezone yang sama?
- Apakah ada nilai acak yang dihitung ulang saat render?
- Apakah komponen membaca
windowataulocalStorageterlalu awal? - Apakah feature flag tersedia dan konsisten di kedua sisi?
- Apakah data async di server identik dengan data yang diterima client saat hydration?
- Apakah environment build, runtime, locale, atau konfigurasi berbeda?
Dengan kata lain, hydration mismatch hampir selalu adalah masalah sinkronisasi asumsi.
Penyebab umum hydration mismatch
1. Waktu lokal, locale, dan timezone
Salah satu sumber mismatch paling sering adalah formatting tanggal dan waktu. Server bisa berjalan di UTC, sementara browser mengikuti timezone pengguna. Jika Anda merender string waktu secara langsung saat render, output bisa berbeda.
// Contoh yang rawan mismatch
function PublishedAt({ date }) {
return <time>{new Date(date).toLocaleString()}</time>;
}Masalahnya bukan pada API tanggal itu sendiri, melainkan pada fakta bahwa toLocaleString() bergantung pada environment. Server dan browser belum tentu punya locale, timezone, atau implementasi format yang identik.
Pola perbaikan:
- Render format yang stabil di server, misalnya ISO atau format yang sudah diproses dari backend.
- Jika harus memakai timezone lokal pengguna, tampilkan placeholder stabil saat SSR lalu format ulang setelah mount.
- Pastikan kebutuhan SEO dan UX seimbang: untuk konten penting, lebih baik kirim string final dari server.
function PublishedAt({ isoString }) {
const [formatted, setFormatted] = React.useState(isoString);
React.useEffect(() => {
setFormatted(new Date(isoString).toLocaleString());
}, [isoString]);
return <time dateTime={isoString}>{formatted}</time>;
}Trade-off-nya: UI mungkin berubah setelah mount. Namun perubahan itu disengaja dan terkendali, bukan mismatch tak terduga.
2. Nilai acak atau ID yang berubah setiap render
Pemanggilan Math.random(), Date.now(), atau generator ID saat render awal adalah jebakan klasik. Server menghasilkan angka A, client menghasilkan angka B, lalu DOM tidak lagi cocok.
// Hindari pola seperti ini saat SSR
function BannerVariant() {
const variant = Math.random() > 0.5 ? 'A' : 'B';
return <div>Eksperimen {variant}</div>;
}Pola perbaikan:
- Hitung nilai acak di server, lalu kirim sebagai props atau payload state.
- Untuk identifier internal, gunakan mekanisme yang dirancang aman untuk SSR dari framework atau library yang dipakai, bukan nilai acak manual saat render.
- Jika eksperimen A/B diputuskan dari cookie atau header, pastikan keputusan yang sama tersedia di server dan client.
3. Akses window, document, atau localStorage saat render
Browser API tidak tersedia di server. Bug yang lebih halus terjadi ketika developer menambahkan fallback saat SSR, tetapi fallback itu menghasilkan UI berbeda dari yang muncul di client.
// Rawan mismatch
function ThemeLabel() {
const theme = typeof window !== 'undefined'
? localStorage.getItem('theme')
: 'light';
return <span>Tema: {theme}</span>;
}Jika browser menyimpan dark, server tetap merender light. Saat hydration, teks berubah. Terkadang hanya menimbulkan flicker, terkadang memicu mismatch warning.
Pola perbaikan:
- Ambil nilai penting dari cookie agar server juga bisa membacanya.
- Jika nilainya murni client-side, tunda pembacaan sampai mount dan render fallback yang stabil.
- Untuk komponen yang benar-benar bergantung pada browser API, pertimbangkan render khusus client.
function ThemeLabel() {
const [theme, setTheme] = React.useState('light');
React.useEffect(() => {
const saved = window.localStorage.getItem('theme');
if (saved) setTheme(saved);
}, []);
return <span>Tema: {theme}</span>;
}Ini aman untuk SSR, tetapi tetap ada kemungkinan UI berubah setelah mount. Jika tema memengaruhi layout besar, lebih baik sinkronkan lewat cookie atau state awal dari server.
4. Feature flag yang tidak konsisten
Feature flag sering dibaca dari layanan remote, cookie, header, atau konfigurasi environment. Jika server menganggap flag aktif tetapi client belum mendapat nilai yang sama saat hydration, subtree UI bisa berbeda total.
Contoh gejala:
- Server menampilkan tombol baru, client menampilkan tombol lama.
- Server merender komponen eksperimen, client tidak.
- Urutan elemen berubah karena flag memengaruhi layout.
Pola perbaikan:
- Jadikan hasil evaluasi flag di server sebagai sumber kebenaran untuk render awal.
- Serialisasikan hasil flag ke payload yang dipakai client saat hydration.
- Hindari evaluasi independen di server dan client jika inputnya belum tentu sama.
5. Data async yang berubah di antara render server dan hydration client
Server mungkin merender data versi lama, lalu client melakukan fetch ulang dan langsung mendapat versi baru. Ini bisa membuat daftar item, jumlah notifikasi, atau status order berubah tepat saat hydration.
Dalam banyak framework SSR, pola yang aman adalah:
- Fetch data di jalur SSR.
- Kirim hasilnya ke client sebagai state awal.
- Gunakan state awal itu untuk render pertama di browser.
- Baru setelah hydration, lakukan revalidasi bila diperlukan.
Masalah biasanya muncul ketika fetch di client tidak memakai payload SSR yang sama, atau komponen melakukan fetch ganda tanpa mekanisme deduplikasi dan state awal yang konsisten.
6. Perbedaan environment dan konfigurasi runtime
Kasus ini paling sulit karena gejalanya sering acak. Contohnya:
- Variabel environment berbeda antara server dan bundle client.
- Locale default di server tidak sama dengan browser pengguna.
- Nilai base URL, region, atau konfigurasi eksperimen berbeda.
- Perbedaan implementasi API tertentu antara runtime server dan browser.
Pelajarannya sama seperti mengelola sistem terdistribusi: jangan mengandalkan asumsi implisit. Jika sebuah nilai memengaruhi output render, tentukan dengan jelas siapa sumber kebenarannya dan bagaimana nilainya dibawa ke kedua sisi.
Gejala khas: render mismatch, state hydration, dan UI yang berubah setelah mount
Tidak semua perubahan UI setelah mount adalah bug. Yang penting adalah membedakan tiga kondisi berikut:
Render mismatch
Server dan client menghasilkan markup awal yang berbeda untuk node yang sama. Ini paling berisiko karena hydration dapat gagal parsial, memaksa framework memperbaiki DOM, atau menghasilkan warning yang sulit dilacak.
State hydration yang benar tetapi memicu re-render setelah mount
Ini bisa normal. Misalnya, Anda sengaja memakai placeholder SSR lalu mengganti nilai dengan preferensi lokal pengguna setelah browser siap. UI berubah, tetapi perubahan itu terkendali dan diharapkan.
UI berubah setelah mount karena sumber data tidak stabil
Ini area abu-abu. Tidak selalu ada warning hydration, tetapi UX buruk: konten meloncat, angka berubah, tombol pindah. Secara teknik mungkin “berhasil”, tetapi model state-nya tetap lemah.
Prinsip praktisnya: render pertama harus deterministik. Kalau harus berubah setelah mount, perubahan itu harus disengaja, lokal, dan punya alasan jelas.
Pola diagnosis yang efektif
Saat menemukan hydration mismatch, jangan langsung menebak-nebak. Lakukan diagnosis seperti menelusuri state yang menyimpang di sistem terdistribusi.
1. Identifikasi node atau komponen pertama yang berbeda
Lihat warning di console dan periksa subtree yang bermasalah. Fokus pada komponen terdekat yang memiliki:
- Formatting tanggal/waktu
- Conditional rendering
- Daftar dengan key dinamis
- Pembacaan browser API
- Feature flag
- Fetch data tambahan
2. Bandingkan input render server vs client
Untuk komponen bermasalah, catat semua input yang memengaruhi output:
- Props
- Cookie
- Header
- Locale
- Timezone
- Environment variable
- State store awal
- Hasil evaluasi flag
Sering kali mismatch bukan pada JSX, melainkan pada perbedaan input ini.
3. Cari pemanggilan API non-deterministik saat render
Periksa apakah ada hal-hal berikut di fungsi render atau computed state awal:
Math.random()Date.now()new Date().toLocaleString()window,document,localStorage,matchMedia
Jika ada, tanyakan: apakah nilai ini bisa sama di server dan client saat render pertama?
4. Uji dengan environment yang lebih terkendali
Gunakan locale tetap, nonaktifkan eksperimen, dan pakai data fixture bila perlu. Tujuannya bukan memperbaiki bug dengan “menghilangkannya”, tetapi mempersempit sumber ketidaksinkronan.
5. Log payload SSR dan state awal client
Untuk kasus data async dan feature flag, penting melihat apakah state yang di-serialize dari server benar-benar dipakai oleh client saat hydration. Jika client segera menghitung ulang dari sumber berbeda, mismatch hampir pasti akan terulang.
Pola perbaikan praktis di Next.js dan Nuxt.js secara umum
Framework berbeda dalam detail implementasi, tetapi pola perbaikannya serupa.
1. Bedakan data untuk SSR dari data yang hanya aman di client
Pertanyaan utamanya: apakah data ini harus menentukan HTML awal?
- Jika ya, pastikan nilainya tersedia di server dan diserialisasikan ke client.
- Jika tidak, render fallback stabil lalu isi nilainya setelah mount.
Contoh data yang cocok untuk fallback client-side:
- Ukuran viewport
- Preferensi dari localStorage
- Status API browser tertentu
2. Kirim state awal sebagai sumber kebenaran
Untuk daftar, profil pengguna, feature flag, dan hasil eksperimen, gunakan payload SSR sebagai state awal pada client. Hindari pola di mana server merender data A tetapi client langsung merender ulang memakai data B sebelum hydration selesai.
3. Gunakan komponen khusus client hanya bila memang perlu
Baik di Next.js maupun Nuxt.js, ada pola untuk merender komponen hanya di client. Ini berguna untuk widget yang sangat bergantung pada browser API atau library pihak ketiga yang tidak aman untuk SSR.
Namun ada trade-off:
- SEO bisa menurun jika konten penting tidak ada di HTML awal.
- Time to interactive dan UX awal bisa terpengaruh.
- Masalah state bisa tersembunyi, bukan benar-benar diselesaikan.
Gunakan pendekatan ini untuk komponen yang memang tidak bernilai jika dipaksa SSR, bukan sebagai solusi default untuk semua mismatch.
4. Stabilkan formatting dan serialisasi data
Jika Anda merender data kompleks seperti tanggal, mata uang, atau teks berbasis locale:
- Format di server menjadi string final bila memungkinkan.
- Atau kirim nilai mentah plus locale/timezone eksplisit yang sama untuk kedua sisi.
- Hindari membiarkan server dan browser memilih default masing-masing.
5. Pastikan conditional rendering tidak bergantung pada sumber yang berbeda
Pola ini sering menimbulkan subtree yang benar-benar berbeda:
{isLoggedIn ? <Dashboard /> : <Landing />}Jika isLoggedIn di server berasal dari cookie tetapi di client berasal dari store yang belum terisi, mismatch mudah terjadi. Solusinya adalah memastikan status autentikasi awal dikirim bersama hasil SSR, bukan dihitung ulang secara independen saat hydration.
6. Tunda efek visual yang tidak penting sampai setelah mount
Untuk personalisasi non-kritis seperti sapaan berdasarkan waktu lokal atau preferensi minor, lebih aman memakai placeholder stabil di SSR lalu memperbarui setelah mount. Kuncinya adalah menjaga perubahan sekecil mungkin agar tidak terasa seperti bug.
Contoh konkret: memperbaiki komponen yang mismatch
Misalkan sebuah halaman SSR menampilkan salam dan tema pengguna:
function Header() {
const hour = new Date().getHours();
const greeting = hour < 12 ? 'Pagi' : 'Sore';
const theme = typeof window !== 'undefined'
? localStorage.getItem('theme') || 'light'
: 'light';
return (
<header data-theme={theme}>
<h2>Selamat {greeting}</h2>
</header>
);
}Komponen ini rawan mismatch karena:
- Jam server bisa berbeda dari jam lokal pengguna.
- Tema dibaca dari
localStorage, yang hanya ada di browser.
Versi yang lebih aman:
function Header({ initialGreeting, initialTheme = 'light' }) {
const [theme, setTheme] = React.useState(initialTheme);
React.useEffect(() => {
const saved = window.localStorage.getItem('theme');
if (saved) setTheme(saved);
}, []);
return (
<header data-theme={theme}>
<h2>Selamat {initialGreeting}</h2>
</header>
);
}Mengapa ini lebih baik?
- Greeting ditentukan di server sebagai input eksplisit, jadi HTML awal stabil.
- Theme memakai nilai awal yang konsisten, lalu boleh diperbarui setelah mount.
- Perubahan setelah mount terbatas pada bagian yang memang bergantung pada browser.
Jika tema penting untuk tampilan awal, langkah yang lebih kuat adalah membaca preferensi dari cookie agar server dan client memiliki nilai awal yang sama.
Checklist pencegahan hydration mismatch
- Pastikan render pertama deterministik. Hindari nilai acak, waktu real-time, dan API browser saat render SSR.
- Tentukan sumber kebenaran. Untuk setiap nilai yang memengaruhi UI awal, jelas siapa yang menetapkan: server, cookie, backend, atau client.
- Serialisasikan state awal. Data SSR harus dipakai ulang oleh client saat hydration, bukan dihitung ulang dengan asumsi berbeda.
- Bedakan data kritis dan non-kritis. Data yang menentukan HTML awal harus sinkron; data personalisasi ringan bisa ditunda sampai mount.
- Hindari default implisit environment. Locale, timezone, dan konfigurasi eksperimen sebaiknya eksplisit bila memengaruhi output.
- Periksa conditional rendering. Cabang UI seperti login, feature flag, dan eksperimen harus memakai input yang sama di server dan client.
- Gunakan client-only rendering dengan sadar. Cocok untuk komponen tertentu, tetapi bukan pengganti desain state yang benar.
- Uji di beberapa environment. Bug hydration sering lolos di mesin developer karena locale, cookie, dan data terlalu stabil.
Kesalahan umum yang sering memperpanjang debugging
- Menganggap warning hydration hanya masalah kosmetik. Kadang benar, tetapi sering menandakan desain state awal yang rapuh.
- Memperbaiki dengan mematikan SSR untuk seluruh halaman. Ini bisa menyembunyikan akar masalah dan mengorbankan manfaat SSR.
- Menyalahkan framework terlalu cepat. Dalam banyak kasus, framework hanya mengungkapkan bahwa input render tidak konsisten.
- Mencampur data SSR dengan fetch client tanpa strategi revalidasi. Hasilnya UI berubah di saat yang salah.
- Tidak mendokumentasikan asumsi state. Tim sering tahu “cara membuatnya jalan”, tetapi tidak tahu mengapa HTML awal harus dibentuk dari input tertentu.
Penutup
Hydration mismatch di SSR jarang merupakan bug misterius. Lebih sering, ia adalah sinyal bahwa server dan client sedang menjalankan dua model state yang berbeda. Di sinilah pelajaran dari cara berpikir tentang konfigurasi dan state pada sistem seperti Kubernetes terasa relevan: sistem akan stabil jika sumber kebenaran jelas, input eksplisit, dan transisi state dapat diprediksi.
Untuk aplikasi SSR modern, prinsipnya sederhana tetapi disiplin: buat render pertama deterministik, kirim state awal yang konsisten, dan tunda hal-hal yang murni client-side sampai setelah mount. Jika Anda memegang tiga prinsip ini, sebagian besar hydration mismatch bisa dicegah sebelum sampai ke production.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!