Hydration drift pada aplikasi SSR biasanya terjadi saat HTML hasil render di server tidak cocok dengan state awal yang dipakai client saat melakukan hydration. Penyebabnya sering bukan sekadar state apa yang dipakai, melainkan di mana aturan ignore state diterapkan: server mengabaikan satu sumber state, sementara client membacanya sejak awal.
Analogi yang berguna berasal dari cara Git mengabaikan file. Masalahnya bukan cuma file mana yang di-ignore, tetapi apakah aturan ignore itu berlaku di level repository, global, atau lokal. Pada SSR, logikanya mirip: kalau server dan client memakai aturan berbeda untuk membaca localStorage, waktu, locale, cookie, media query, atau feature flag, maka markup awal akan bergeser. Hasilnya bisa berupa warning hydration mismatch, UI berkedip, event handler menempel ke node yang salah, atau state awal yang terasa acak.
Kenapa hydration drift terjadi
Pada SSR, alurnya umumnya seperti ini:
- Server merender HTML berdasarkan state yang tersedia saat request diproses.
- Browser menerima HTML dan menampilkannya.
- Client menjalankan JavaScript, membangun tree komponen yang sama, lalu melakukan hydration.
- Jika hasil render client pertama berbeda dari HTML server, framework akan memberi warning atau melakukan koreksi DOM.
Masalah utama muncul saat render pertama di server dan render pertama di client tidak deterministik. Ini sering terjadi bila salah satu sisi membaca sumber state yang tidak tersedia, tidak konsisten, atau sengaja diabaikan secara berbeda.
Gejala umum render mismatch
- Warning seperti Text content does not match server-rendered HTML.
- Elemen yang sempat tampil lalu berubah setelah hydration.
- Tema gelap/terang berkedip saat halaman dibuka.
- Format tanggal atau angka berbeda antara server dan browser pengguna.
- Komponen yang bergantung pada viewport tampil berbeda sebelum dan sesudah hydration.
- UI A/B test atau feature flag berubah setelah JavaScript aktif.
Bukan hanya apa yang di-ignore, tetapi di mana aturan ignore diterapkan
Dalam konteks SSR, “ignore state” berarti salah satu sisi sengaja atau tidak sengaja mengabaikan sumber state tertentu saat render awal. Contoh:
- Server tidak punya akses ke
localStorage, tetapi client membacanya saat render pertama. - Server memakai locale default, client memakai locale browser.
- Server tidak tahu ukuran viewport, client langsung menghitung
matchMedia. - Server belum menerima flag eksperimen yang sama dengan client.
Jika aturan ini diterapkan di tempat yang berbeda, hasil render akan drift. Jadi fokusnya bukan “jangan pakai localStorage” atau “jangan pakai waktu saat SSR”, melainkan samakan aturan pembacaan state pada fase render awal.
Prinsip praktis: untuk render pertama, server dan client harus berbagi sumber kebenaran yang sama, atau sama-sama menunda pembacaan state yang tidak stabil sampai setelah hydration.
Sumber state non-deterministik yang paling sering memicu mismatch
1. localStorage dan sessionStorage
Ini sumber klasik. Server tidak bisa membacanya, tetapi client bisa. Jika nilai dari storage dipakai langsung untuk menentukan teks, tema, layout, atau visibilitas komponen pada render pertama, mismatch mudah terjadi.
Anti-pattern:
// Render awal client membaca localStorage, server tidak bisa.
function ThemeLabel() {
const theme = typeof window !== 'undefined'
? localStorage.getItem('theme') || 'light'
: 'light';
return <span>Tema: {theme}</span>;
}Server mungkin menghasilkan Tema: light, sementara client langsung menghitung Tema: dark. Hydration drift pun terjadi.
2. Waktu dan tanggal
Date.now(), new Date(), atau formatter tanggal yang bergantung pada zona waktu lokal sering menghasilkan output berbeda antara server dan browser.
Anti-pattern:
function Clock() {
return <time>{new Date().toLocaleTimeString()}</time>;
}Beberapa milidetik saja cukup untuk menghasilkan string berbeda saat hydration.
3. Locale dan format internasional
Server bisa memakai locale default proses atau locale dari request, sementara client memakai locale browser. Ini bisa memengaruhi format tanggal, mata uang, pluralisasi, dan urutan teks.
4. Media query dan viewport
Server tidak tahu hasil matchMedia atau ukuran viewport aktual. Jika layout atau label ditentukan langsung dari kondisi itu saat render pertama, hasilnya berpotensi berbeda.
5. Cookie dan session
Cookie sebenarnya lebih aman untuk SSR karena dapat dibaca server dari request. Masalah muncul saat client memakai cookie atau state turunan yang berbeda dari yang dipakai server, misalnya karena pembacaan dilakukan terlalu dini atau ada fallback yang tidak sama.
6. Feature flag dan eksperimen
Jika server merender varian A tetapi client mengambil flag terbaru dan merender varian B saat hydration, Anda akan melihat drift pada struktur DOM, bukan hanya teks.
Pola anti-pattern yang perlu dihindari
Membaca browser-only API di render path
Walau dibungkus dengan cek typeof window !== 'undefined', hasilnya belum tentu aman. Cek itu hanya mencegah crash di server, tetapi tidak menjamin output render pertama sama.
Menggunakan fallback berbeda antara server dan client
Contoh: server fallback ke light, client fallback ke preferensi sistem. Secara teknis keduanya “benar”, tetapi tidak identik.
Menyelipkan nilai non-deterministik ke JSX/template
Nilai seperti waktu sekarang, random ID, atau hasil formatter yang bergantung environment sebaiknya tidak muncul langsung pada HTML awal kecuali memang diserialisasi dari server.
Menggabungkan beberapa sumber state tanpa prioritas yang jelas
Misalnya tema diambil dari URL, lalu cookie, lalu localStorage, lalu media query, tetapi urutan server dan client berbeda. Ini sering terjadi saat utilitas untuk SSR dan browser dibuat terpisah.
Strategi utama: samakan sumber kebenaran
Solusi yang paling stabil adalah menentukan satu aturan resolusi state awal yang bisa dipakai pada server dan client. Jika sebuah sumber state tidak tersedia secara simetris, jangan dipakai untuk menentukan HTML awal.
Pola 1: Pakai state yang diserialisasi dari server
Server membaca sumber yang tersedia saat request, lalu mengirimkan state awal ke client. Client wajib memulai dari nilai yang sama sebelum melakukan sinkronisasi lanjutan.
// Pseudocode lintas framework
// Server:
const initialTheme = readThemeFromCookie(request) || 'light';
renderPage({ initialTheme });
// Client hydration:
const theme = initialTheme; // harus sama dengan yang dipakai server
// Setelah mount, baru sinkronkan jika perlu dengan localStorage atau media queryPola ini cocok untuk tema, locale, preferensi pengguna, dan feature flag yang bisa diketahui dari request.
Pola 2: Tunda pembacaan state browser sampai setelah hydration
Jika state hanya tersedia di browser, render placeholder atau nilai netral yang sama di server dan client, lalu perbarui setelah mount.
// React/Next.js style
function ClientThemeLabel({ initialTheme = 'light' }) {
const [theme, setTheme] = useState(initialTheme);
useEffect(() => {
const saved = localStorage.getItem('theme');
if (saved) setTheme(saved);
}, []);
return <span>Tema: {theme}</span>;
}Di sini, render pertama tetap konsisten. Perubahan baru terjadi setelah hydration selesai.
Pola 3: Gunakan cookie sebagai jembatan SSR-client
Untuk preferensi seperti tema atau locale, cookie sering menjadi sumber kebenaran yang lebih simetris dibanding localStorage. Server dapat membacanya dari request, client dapat memperbaruinya setelah interaksi pengguna.
Kapan memilih cookie:
- Saat nilai dibutuhkan untuk HTML awal.
- Saat state harus dibaca di server dan client.
- Saat Anda ingin menghindari flicker pada tema atau personalisasi ringan.
Trade-off:
- Perlu disiplin sinkronisasi jika juga menyimpan di storage lain.
- Tidak cocok untuk data besar.
- Harus memperhatikan keamanan dan atribut cookie yang sesuai.
Deferred client render dan guard hydration
Tidak semua state perlu tampil di HTML awal. Untuk bagian UI yang sangat bergantung pada browser, pendekatan yang lebih aman adalah deferred client render: komponen dirender minimal saat SSR, lalu isi aktual muncul setelah mount.
Contoh guard hydration
// Pola umum lintas framework
function BrowserOnly({ children, fallback = null }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted ? children : fallback;
}Pola ini berguna untuk:
- Komponen yang membaca viewport.
- Integrasi dengan library yang mengakses DOM saat inisialisasi.
- Widget yang bergantung pada storage browser.
Kelemahan:
- Ada penundaan tampilan konten.
- Bisa memengaruhi CLS atau persepsi performa jika fallback tidak dirancang baik.
- Kurang ideal untuk konten penting di atas lipatan layar.
Kapan guard hydration tepat digunakan
Gunakan guard bila state tidak bisa disamakan secara realistis antara server dan client. Namun, jangan menjadikannya solusi default untuk semua mismatch. Jika state sebenarnya bisa dipindahkan ke cookie, request context, atau payload server, itu biasanya lebih baik.
Contoh audit: tema, locale, dan viewport
Kasus 1: Tema gelap dari localStorage
Masalah: server selalu merender tema terang, client membaca localStorage dan langsung mengganti ke gelap saat hydration.
Perbaikan:
- Pindahkan preferensi tema ke cookie agar server bisa membacanya.
- Gunakan nilai cookie sebagai initial state di server dan client.
- Setelah mount, sinkronkan ke
localStoragehanya jika memang masih diperlukan.
Kasus 2: Format tanggal berbeda
Masalah: server memakai locale default proses, browser memakai locale pengguna.
Perbaikan:
- Tentukan locale dari request atau setting pengguna.
- Serialisasikan locale itu ke client.
- Pastikan formatter di kedua sisi memakai locale yang sama.
- Jika waktu real-time dibutuhkan, render placeholder statis lalu update setelah mount.
Kasus 3: Layout desktop/mobile dari media query
Masalah: server merender versi desktop, client di ponsel langsung memilih mobile.
Perbaikan:
- Hindari menentukan struktur DOM utama dari
matchMediasaat render awal. - Utamakan CSS responsif untuk perbedaan presentasional.
- Jika perilaku JavaScript memang berbeda, aktifkan setelah hydration.
Contoh implementasi resolver state awal
Daripada setiap komponen punya logika sendiri, buat resolver terpusat untuk menentukan state awal. Ini mengurangi risiko aturan server dan client menyimpang.
// Pseudocode: satu aturan resolusi
function resolveInitialPrefs(context) {
return {
theme: context.cookieTheme || 'light',
locale: context.requestLocale || 'id-ID',
featureX: Boolean(context.serverFlags?.featureX)
};
}
// Server memakai resolver ini saat render.
// Client memulai hydration dari payload hasil resolver yang sama.Kenapa ini bekerja: logika prioritas state tidak tersebar ke banyak komponen. Anda punya satu sumber kebenaran untuk render awal, lalu semua sinkronisasi browser dilakukan sebagai langkah kedua, bukan bagian dari hydration.
Panduan praktis untuk Next.js, Nuxt, dan SvelteKit
Walau detail API tiap framework berbeda, prinsipnya sama:
- Jangan membaca state browser-only untuk menentukan HTML awal jika server tidak punya akses ke nilai yang sama.
- Gunakan data request seperti cookie, header, session, atau payload server untuk state awal yang memengaruhi markup.
- Tunda pembacaan browser-only state ke fase mount/hydrated untuk komponen yang tidak kritis pada SSR.
- Hindari logika bercabang berbeda antara server dan client dalam path render yang sama.
Pada framework SSR modern, Anda biasanya memiliki:
- mekanisme untuk membaca cookie atau request context di server,
- cara mengirimkan data awal ke halaman,
- hook lifecycle setelah mount di browser.
Fokuskan desain state Anda pada tiga hal itu, bukan pada trik untuk “membungkam” warning hydration.
Checklist debugging hydration drift
1. Cari node pertama yang mismatch
Jangan mulai dari seluruh halaman. Temukan komponen atau elemen pertama yang berbeda antara HTML server dan render client.
2. Audit semua input render pertama
Tanyakan pada setiap komponen:
- Apakah komponen ini membaca
window,document, storage, waktu, random, locale browser, atau media query? - Apakah server memiliki nilai yang sama?
- Jika tidak, kenapa komponen ini tetap menentukan HTML awal dari sumber itu?
3. Bandingkan fallback server dan client
Pastikan fallback benar-benar identik, bukan sekadar “mirip”. Perbedaan teks, atribut, urutan child, atau class dapat memicu mismatch.
4. Periksa formatter internasional
Verifikasi locale, timezone, dan opsi format yang dipakai di kedua sisi.
5. Isolasi feature flag
Pastikan flag yang memengaruhi struktur DOM dikirim dari server ke client sebagai payload awal yang sama.
6. Kurangi non-determinism
Hindari Date.now(), Math.random(), dan pembacaan environment yang berubah-ubah di path render awal.
7. Uji dengan JavaScript lambat
Throttle jaringan dan CPU di browser. Banyak drift hanya terlihat jelas saat jeda antara SSR dan hydration lebih panjang.
Langkah audit komponen yang bisa langsung diterapkan
- Daftar komponen SSR yang paling sering memicu warning.
- Tandai semua pembacaan state di dalam render: storage, cookie, waktu, locale, media query, flag, random.
- Klasifikasikan sumber state: tersedia di server, hanya tersedia di client, atau bergantung environment.
- Tentukan sumber kebenaran awal yang sama untuk server dan client.
- Pindahkan sinkronisasi browser-only ke hook setelah mount/hydrated.
- Gunakan placeholder atau fallback stabil bila state belum tersedia saat SSR.
- Uji beberapa skenario: user baru, user dengan cookie lama, locale berbeda, mode gelap, viewport mobile, dan flag eksperimen aktif.
Kapan mismatch bisa diterima?
Secara umum, mismatch sebaiknya dianggap bug. Namun ada kasus di mana Anda sengaja menunda render konten tertentu ke client, misalnya widget analitik, editor rich text, atau komponen yang benar-benar browser-only. Dalam kasus seperti itu, yang penting adalah:
- server dan client sama-sama menghasilkan fallback awal yang konsisten,
- komponen aktual baru dirender setelah hydration,
- dampak UX dan SEO dipahami dengan jelas.
Penutup
SSR stabil tidak cukup dicapai dengan daftar state yang “aman” atau “tidak aman”. Kuncinya adalah memastikan aturan ignore state diterapkan secara konsisten. Jika server mengabaikan sebuah sumber state, client juga tidak boleh memakainya untuk render pertama. Sebaliknya, jika nilai itu penting untuk HTML awal, pindahkan ke sumber kebenaran yang bisa diakses kedua sisi, seperti cookie atau payload server.
Dengan pola ini, Anda tidak hanya menghilangkan warning hydration mismatch, tetapi juga membuat perilaku UI lebih dapat diprediksi, lebih mudah diaudit, dan lebih tahan terhadap perubahan framework maupun arsitektur aplikasi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!