Hydration drift adalah kondisi ketika HTML yang dikirim server terlihat benar, tetapi saat JavaScript di browser aktif, UI langsung berubah karena state awal klien tidak sama dengan hasil render server. Dalam aplikasi SSR modern, masalah ini sering muncul bukan karena framework "gagal hydrate", melainkan karena asumsi render kita salah: server dan klien menghitung output dari input yang berbeda.
Intinya sederhana: SSR bukan sumber kebenaran jika data, waktu, locale, random value, media query, localStorage, atau feature flag dibaca dengan cara yang berbeda antara server dan browser. Pendekatan yang lebih aman adalah berhenti menebak, lalu memverifikasi asumsi render di setiap komponen yang berpotensi nondeterministik.
Mengapa hydration drift terjadi
Hydration bekerja dengan asumsi bahwa struktur HTML dan state awal yang dipakai klien setara dengan yang dipakai server. Bila server merender "Selamat pagi" tetapi klien pada render pertama memutuskan seharusnya "Selamat malam", framework harus memilih antara memperingatkan mismatch, membuang subtree, atau merender ulang bagian tertentu. Akibatnya bisa berupa flicker, warning di console, event handler yang menempel terlambat, atau UI yang tampak "melompat".
Masalah ini sering salah didiagnosis sebagai bug SSR, padahal akar masalahnya adalah input render yang tidak deterministik. Jika dua environment menghasilkan keputusan UI yang berbeda dari input yang sama, atau lebih buruk lagi dari input yang sebenarnya berbeda, hydration drift hampir pasti muncul.
Gejala yang umum terlihat
- Warning mismatch saat hydration di console browser.
- Teks berubah sesaat setelah halaman tampil.
- Layout bergeser setelah mount.
- Komponen tertentu selalu merender ulang di klien meski data terlihat sama.
- Tombol, label, atau varian komponen berbeda antara SSR dan render pertama klien.
- Bug yang hanya muncul pada zona waktu, bahasa, atau perangkat tertentu.
Root cause: server dan klien membaca realitas yang berbeda
1. Waktu dan zona waktu
Waktu adalah penyebab klasik. Server bisa merender berdasarkan zona waktu server atau waktu request, sedangkan klien memakai waktu lokal pengguna. Bahkan selisih beberapa detik bisa mengubah output jika UI bergantung pada new Date() saat render.
// Anti-pattern: nilai waktu dihitung saat render di dua environment berbeda
function Greeting() {
const hour = new Date().getHours()
const text = hour < 12 ? 'Selamat pagi' : 'Selamat siang'
return <p>{text}</p>
}Mengapa ini bermasalah: server dan klien bisa berada di zona waktu berbeda, atau hydration terjadi setelah waktu berganti.
Pendekatan yang lebih aman:
- Hitung nilai di server lalu kirim sebagai props serializable.
- Jika harus berbasis waktu lokal pengguna, tampilkan placeholder netral saat SSR lalu hitung setelah mount.
- Untuk timestamp, kirim nilai mentah yang sama ke klien, lalu format ulang setelah hydration jika memang perlu mengikuti locale pengguna.
// Lebih aman: server mengirim nilai mentah yang stabil
function PublishedAt({ iso }) {
return <time dateTime={iso}>{iso}</time>
}
// Setelah mount, Anda boleh mengganti tampilan menjadi format lokal pengguna.2. Locale dan format angka/tanggal
Intl dan locale pengguna sering menghasilkan string yang berbeda antara server dan browser. Server mungkin default ke en-US, klien ke id-ID, atau punya perbedaan implementasi kecil dalam formatting.
// Anti-pattern: locale implisit dan tidak dikontrol
const price = new Intl.NumberFormat().format(1200000)Perbaikannya:
- Tentukan locale secara eksplisit jika output harus sama di server dan klien.
- Jika locale harus mengikuti browser, jangan jadikan hasil format lokal sebagai bagian dari HTML SSR final tanpa strategi fallback.
- Simpan nilai mentah untuk source of truth; string terformat hanyalah representasi.
3. Random value dan ID yang tidak stabil
Math.random(), UUID yang dibuat saat render, atau ID acak untuk atribut DOM adalah sumber mismatch yang mudah luput.
// Anti-pattern
function PromoBanner() {
const variant = Math.random() > 0.5 ? 'A' : 'B'
return <div>Varian {variant}</div>
}Jika server memilih A dan klien memilih B, subtree langsung berbeda. Solusi yang benar adalah membuat keputusan sekali, lalu meneruskan hasilnya ke render server dan klien. Misalnya, putuskan variant di request layer atau backend, simpan dalam payload awal, dan hydrate dengan nilai yang sama.
4. Data async yang berubah di antara SSR dan hydration
Data yang diambil saat SSR bisa kadaluarsa beberapa milidetik kemudian, lalu klien mengambil ulang dan mendapat hasil berbeda sebelum hydration selesai atau segera sesudahnya. Ini bukan selalu salah, tetapi jika data itu memengaruhi struktur awal DOM, drift akan tampak jelas.
Contoh umum:
- Hit counter, stok barang, status online, notifikasi belum dibaca.
- Daftar yang diurutkan berdasarkan data real-time.
- Feature flag yang dibaca ulang dari endpoint berbeda di klien.
Strateginya:
- Pisahkan initial snapshot dari live update.
- Hydrate dengan payload data yang sama persis dengan SSR.
- Lakukan revalidasi setelah mount, bukan dengan mengganti sumber data saat render pertama klien.
5. Media query dan ukuran viewport
Server tidak tahu ukuran viewport aktual, preferensi reduced motion, atau mode warna pengguna kecuali Anda memberi petunjuk eksplisit. Komponen yang langsung merender cabang berbeda berdasarkan matchMedia hampir pasti rawan mismatch.
// Anti-pattern konseptual
const isMobile = window.matchMedia('(max-width: 768px)').matches
return isMobile ? <MobileNav /> : <DesktopNav />Perbaikannya:
- Utamakan CSS responsif untuk perbedaan presentasional.
- Jika struktur HTML harus berbeda, render fallback netral yang aman untuk SSR.
- Jangan membaca
windowataumatchMediasaat render awal yang harus cocok dengan SSR.
6. localStorage, cookie klien, dan state personal
Nilai dari localStorage tidak tersedia saat SSR. Jika Anda langsung menggunakannya untuk menentukan tema, urutan daftar, tab aktif, atau status login visual, hasil klien bisa berbeda dari HTML server.
// Anti-pattern
function ThemeLabel() {
const theme = localStorage.getItem('theme') || 'light'
return <span>Tema: {theme}</span>
}Solusinya tergantung kebutuhan:
- Jika state perlu diketahui server, simpan di cookie yang juga dibaca saat SSR.
- Jika tidak penting untuk SSR, render nilai default yang stabil lalu sinkronkan setelah mount.
- Untuk tema, pertimbangkan strategi yang mengatur atribut HTML lebih awal sebelum aplikasi hydrate, sehingga perubahan visual tidak terlambat.
7. Feature flag dan eksperimen
Feature flag adalah penyebab yang sangat sering diremehkan. Masalah muncul ketika evaluasi flag dilakukan dengan konteks berbeda: server memakai identitas anonim A, klien memakai identitas B yang baru tersedia setelah load, atau SDK klien memuat konfigurasi yang belum sinkron dengan payload SSR.
Prinsip yang aman:
- Evaluasi flag untuk render awal di satu tempat, idealnya saat request ditangani.
- Serialisasikan hasil evaluasi ke klien.
- Jangan evaluasi ulang secara independen pada render pertama klien jika hasilnya memengaruhi markup awal.
Anti-pattern yang sering membuat SSR terasa benar, padahal tidak
Render yang bergantung pada environment
Bug paling umum bukan kode yang rusak, melainkan kode yang berbeda perilaku berdasarkan environment.
// Waspadai cabang seperti ini dalam render
const isClient = typeof window !== 'undefined'
return <div>{isClient ? 'Klien' : 'Server'}</div>Secara teknis cabang ini valid, tetapi jika hasilnya masuk ke HTML awal, Anda sedang memastikan mismatch.
Menganggap data "hampir sama" itu cukup
Pada hydration, hampir sama sering tidak cukup. Selisih urutan item, whitespace signifikan, atribut, atau jumlah node bisa memicu warning dan patch yang tidak Anda harapkan.
Menyembunyikan gejala alih-alih memperbaiki sumber input
Beberapa framework menyediakan escape hatch untuk menekan warning mismatch. Itu kadang perlu untuk kasus sangat spesifik, tetapi bukan solusi umum. Jika dipakai terlalu cepat, Anda kehilangan sinyal bahwa render Anda nondeterministik.
Budaya kerja yang sehat untuk SSR adalah: jangan percaya bahwa output render "pasti sama" hanya karena terlihat benar di laptop Anda. Verifikasi input dan asumsi render, terutama pada komponen yang membaca waktu, locale, storage, viewport, atau flag.
Strategi perbaikan yang benar-benar mengurangi hydration drift
1. Bedakan data deterministik dan data client-only
Tanyakan pada setiap komponen:
- Apakah output HTML awal harus stabil dan bisa dihasilkan server?
- Apakah nilai yang dipakai tersedia identik di server dan klien?
- Jika tidak, apakah UI bisa memakai placeholder netral sampai mount?
Komponen yang jawabannya "tidak" untuk poin kedua sebaiknya diperlakukan sebagai client-enhanced UI, bukan SSR truth.
2. Kirim snapshot awal yang eksplisit
Untuk data request-scoped seperti user, locale, A/B variant, feature flags, dan hasil query, kirim satu snapshot serializable dari server ke klien. Render pertama klien harus membaca snapshot ini, bukan menghitung ulang dari sumber lain.
// Bentuk payload awal yang aman secara konsep
{
"now": "2026-04-10T08:00:00.000Z",
"locale": "id-ID",
"flags": { "newCheckout": false },
"userPreferences": { "currency": "IDR" }
}Mengapa ini efektif: Anda membuat input render eksplisit dan dapat diaudit. Saat mismatch terjadi, Anda bisa membandingkan payload SSR dengan apa yang dipakai klien.
3. Tunda keputusan yang memang hanya diketahui browser
Beberapa hal memang baru diketahui setelah aplikasi berjalan di browser, misalnya viewport, media query, localStorage, atau bahasa yang benar-benar dipilih user agent. Untuk kasus seperti ini:
- Render versi netral di server.
- Hydrate tanpa cabang struktural yang berubah total.
- Setelah mount, tingkatkan UI secara bertahap.
Trade-off-nya adalah Anda mungkin mendapat satu fase tampilan transisional, tetapi itu lebih terkendali daripada mismatch struktural.
4. Gunakan CSS untuk variasi visual, bukan JS saat render awal
Jika perbedaan hanya soal tampilan mobile/desktop, mode gelap/terang, atau visibilitas elemen, CSS biasanya lebih aman daripada membuat dua pohon DOM berbeda saat render awal.
Pilih JS hanya jika benar-benar perlu mengubah perilaku atau struktur yang tidak bisa diwakili secara wajar oleh CSS.
5. Jaga serialisasi tetap sederhana
Payload SSR yang berisi object kompleks, instance kelas, atau nilai yang berubah bentuk saat serialisasi dapat memicu ketidaksesuaian halus. Simpan state awal dalam bentuk JSON sederhana: string, number, boolean, array, object datar bila memungkinkan.
Guardrail kode untuk Next.js, Nuxt, dan SvelteKit secara framework-agnostik
Nama API berbeda antar framework, tetapi guardrail berikut berlaku umum:
Jangan baca sumber nondeterministik langsung di body render
- Hindari
new Date(),Math.random(),window,document,localStorage,matchMedia, dan evaluasi flag async langsung pada render awal. - Jika perlu, baca nilainya di server lalu injeksikan sebagai props/store awal, atau baca setelah mount.
Centralize request context
Buat satu lapisan yang menyatukan nilai seperti locale, timezone yang dipilih aplikasi, cookie preferensi, user identity, dan feature flags. Komponen tidak boleh mengambil keputusan awal dari banyak sumber yang belum tentu sinkron.
Bedakan server-safe helper dan client-only helper
Praktik yang berguna adalah memisahkan utilitas seperti:
formatCurrencyStable(value, locale)untuk output yang harus sama di SSR dan klien.readViewport()ataureadLocalPreference()yang hanya boleh dipanggil setelah mount.
Buat wrapper untuk komponen rawan drift
Komponen seperti time label, personalized greeting, responsive nav, atau feature-gated CTA sebaiknya dibungkus pola yang jelas: render stabil dulu, enhance kemudian.
// Contoh pola generik
function ClientResolved({ fallback, renderClient }) {
const mounted = useMountedLikeHook() // nama hook bergantung framework
return mounted ? renderClient() : fallback
}Nama hook dan implementasi spesifik berbeda, tetapi idenya sama: jangan paksa SSR menghasilkan markup yang bergantung pada informasi yang belum tersedia di server.
Sediakan mode debug hydration
Di environment pengembangan, log input render penting untuk komponen sensitif:
- timestamp yang dipakai
- locale efektif
- hasil evaluasi feature flag
- sumber preferensi tema
- apakah data berasal dari snapshot SSR atau fetch ulang klien
Tujuannya bukan menambah logging permanen, melainkan mempercepat verifikasi asumsi.
Contoh audit komponen yang sering membingungkan pengguna
Kasus 1: label waktu relatif
Komponen seperti "5 menit lalu" sering drift karena server dan klien menghitung selisih waktu di momen berbeda.
Audit:
- Apakah perhitungan memakai
Date.now()langsung saat render? - Apakah server mengirim timestamp sumber yang sama?
- Apakah label perlu akurat sejak byte pertama, atau cukup akurat setelah mount?
Perbaikan: render timestamp absolut atau string stabil saat SSR, lalu ubah ke waktu relatif setelah mount dan update dengan interval jika diperlukan.
Kasus 2: tema gelap/terang
Pengguna sering melihat flicker tema atau mismatch label tombol tema.
Audit:
- Apakah tema dibaca dari localStorage saja?
- Apakah SSR memakai default light sementara browser user dark?
- Apakah label teks tema ikut dirender sebelum sumber preferensi final diketahui?
Perbaikan: jika tema penting bagi render awal, simpan preferensi di cookie yang dapat dibaca server, atau atur atribut tema sebelum hydration. Untuk label, gunakan fallback stabil sampai state final tersedia.
Kasus 3: navigasi responsif
Dropdown mobile vs navbar desktop sering dipecah ke dua cabang render berdasarkan viewport.
Audit:
- Apakah keputusan struktur dibuat dengan
window.innerWidthataumatchMediasaat render? - Apakah sebenarnya cukup memakai CSS responsif?
Perbaikan: satukan struktur dasar jika memungkinkan, lalu gunakan CSS untuk adaptasi layout. Jika perilaku interaktif berbeda, aktifkan behavior khusus setelah mount tanpa mengubah HTML awal secara drastis.
Kasus 4: CTA berbasis feature flag
Tombol atau flow checkout baru sering hanya muncul di klien setelah SDK flag selesai memuat.
Audit:
- Apakah server dan klien mengevaluasi flag dengan user context yang sama?
- Apakah hasil flag SSR dikirim ke klien?
- Apakah render pertama klien memercayai snapshot itu atau menghitung ulang?
Perbaikan: snapshot hasil evaluasi di server dan gunakan snapshot itu untuk hydration. Re-sync boleh dilakukan setelah mount bila ada alasan kuat.
Checklist debugging hydration drift
- Identifikasi node yang mismatch. Cari komponen terdekat, bukan hanya baris warning yang paling atas.
- Bandingkan input render server vs klien. Catat props, snapshot data, locale, timezone, dan feature flag.
- Cari sumber nondeterministik. Audit penggunaan waktu, random, storage, media query, dan browser API.
- Periksa urutan fetch. Pastikan render pertama klien memakai data yang sama dengan SSR sebelum revalidasi.
- Kurangi area perubahan. Ganti cabang struktural dengan placeholder stabil atau CSS jika memungkinkan.
- Verifikasi serialisasi. Pastikan nilai yang dikirim server benar-benar sama bentuknya saat dibaca klien.
- Uji pada locale dan timezone berbeda. Bug ini sering tidak muncul di mesin developer.
- Uji dengan jaringan lambat. Drift akibat refresh data atau SDK klien sering lebih jelas pada kondisi lambat.
Pola implementasi yang aman untuk banyak kasus
Pola A: SSR stabil, enhancement setelah mount
Cocok untuk waktu lokal, media query, localStorage, atau preferensi browser. Anda menerima fallback awal yang netral demi menjaga konsistensi HTML.
Pola B: Snapshot server sebagai input render tunggal
Cocok untuk data async, user session, locale aplikasi, dan feature flag. Semua keputusan awal dibuat sekali di server lalu diteruskan ke klien.
Pola C: CSS-first responsive rendering
Cocok untuk layout dan visibilitas elemen. Mengurangi keputusan struktural pada render JS awal.
Pola D: Explicit unstable boundary
Jika ada area UI yang memang tidak bisa deterministik saat SSR, tandai secara sadar sebagai boundary client-only atau deferred. Ini lebih jujur dan lebih mudah dipelihara daripada menyamarkan nondeterminisme di seluruh pohon komponen.
Trade-off yang perlu dipahami
- Fallback netral bisa mengurangi personalisasi awal, tetapi meningkatkan stabilitas hydration.
- Snapshot SSR menambah payload, tetapi mempermudah debugging dan menjaga konsistensi.
- Menunda render browser-only state dapat menimbulkan update sesudah mount, tetapi lebih aman daripada mismatch struktural.
- CSS-first tidak selalu cukup untuk kasus perilaku kompleks, tetapi sangat efektif untuk mencegah drift visual.
Penutup
Hydration drift jarang selesai hanya dengan "mencoba-coba sampai warning hilang". Masalah ini lebih mirip persoalan disiplin render: apakah server dan klien benar-benar memakai input yang sama untuk menghasilkan HTML awal yang sama? Jika jawabannya tidak, SSR bukan sumber kebenaran—hanya snapshot sementara yang bisa langsung dibantah oleh browser.
Cara mencegahnya adalah dengan memverifikasi asumsi, bukan mengikuti intuisi. Audit komponen yang membaca waktu, locale, random value, data async, media query, localStorage, dan feature flag. Putuskan mana yang harus deterministik sejak SSR, mana yang perlu snapshot eksplisit, dan mana yang sebaiknya ditunda sampai mount. Dengan guardrail ini, UI SSR modern akan jauh lebih stabil, bisa diprediksi, dan lebih mudah di-debug.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!