Hydration mismatch terjadi ketika HTML hasil server-side rendering (SSR) tidak cocok dengan hasil render pertama di browser. Dampaknya bukan cuma warning di console: framework bisa membuang DOM yang sudah ada, merender ulang subtree, memasang event listener ulang, lalu UI terasa patah-patah atau tersendat pada interaksi awal.
Penyebabnya sering lebih halus daripada sekadar memakai window saat SSR. Salah satu sumber masalah yang sering luput adalah urutan render dan pola akses data yang berbeda antara server dan client: urutan pembacaan state, iterasi list, penyortiran, dan transformasi data yang tidak deterministik. Ide ini mirip dengan pembahasan performa CPU tentang data access patterns: urutan akses data memengaruhi perilaku sistem. Dalam konteks SSR, urutan akses data memengaruhi bentuk output HTML. Jika urutan itu berubah, hasil render juga berubah.
Kenapa urutan akses data bisa memicu hydration mismatch?
Saat SSR, framework menjalankan komponen untuk menghasilkan HTML statis. Di browser, komponen dijalankan lagi untuk proses hydration. Agar hydration berhasil mulus, hasil render awal di browser harus identik dengan HTML dari server, setidaknya pada bagian yang di-hydrate.
Masalah muncul ketika langkah-langkah berikut tidak menghasilkan urutan yang sama di dua lingkungan:
- membaca state awal dari beberapa sumber,
- mengiterasi daftar data,
- melakukan
sort, - melakukan
map,filter,reduce, atau normalisasi data, - membangun key untuk elemen list.
Jika urutan item berubah, maka susunan node HTML juga berubah. Framework lalu melihat bahwa teks, atribut, atau struktur anak tidak cocok dengan yang dikirim server.
Inti masalahnya: hydration membutuhkan render awal yang deterministik. Begitu ada transformasi yang bergantung pada lingkungan, waktu, urutan akses, atau data default yang berbeda, mismatch sangat mudah terjadi.
Gejala hydration mismatch dari urutan render
Bug ini sering tampak seperti masalah performa, padahal akar masalahnya adalah ketidakcocokan render.
Gejala yang umum terlihat
- Warning seperti “hydration failed”, “text content does not match”, atau “mismatched node”.
- Urutan item list berubah setelah halaman tampil.
- Konten sempat tampil benar, lalu “meloncat” ke susunan lain.
- Input kehilangan fokus saat mount.
- Event listener terasa terlambat aktif.
- Komponen tertentu dirender ulang penuh di client.
Kenapa UI terasa tersendat?
Ketika mismatch terjadi, framework sering harus memilih antara mencoba menambal DOM atau membuang subtree tertentu lalu merender ulang di client. Pekerjaan tambahan ini menambah beban CPU pada saat awal halaman dipakai. Jadi, walaupun bug utamanya adalah mismatch, gejala yang dirasakan pengguna adalah lag saat interaksi pertama.
Root cause: urutan render dan pola akses data yang berbeda
1. Sort yang tidak stabil atau pembanding yang tidak deterministik
Kasus paling sering adalah list yang diurutkan dengan comparator yang tidak konsisten. Contoh buruk:
const items = products.sort((a, b) => {
if (a.featured) return -1
if (b.featured) return 1
return 0
})Comparator di atas hanya mengutamakan featured, tetapi tidak memberi aturan jelas ketika dua item sama-sama featured atau sama-sama bukan featured. Jika urutan input berbeda antara server dan client, hasil akhirnya bisa berbeda.
Lebih aman gunakan pembanding total yang punya tie-breaker jelas:
const items = [...products].sort((a, b) => {
if (a.featured !== b.featured) return a.featured ? -1 : 1
if (a.priority !== b.priority) return a.priority - b.priority
return a.id.localeCompare(b.id)
})Kenapa ini bekerja? Karena setiap pasangan elemen memiliki aturan urutan yang konsisten. Hasil sort menjadi lebih deterministik.
2. Transformasi data bergantung pada runtime browser
Contoh lain: server merender dengan locale default tertentu, tetapi browser memakai locale pengguna.
const items = [...products].sort((a, b) =>
a.name.localeCompare(b.name)
)Jika aturan perbandingan string bergantung pada locale lingkungan dan locale tidak disamakan, urutan bisa berubah. Bila Anda perlu mengurutkan teks untuk SSR, pastikan aturan perbandingan eksplisit dan konsisten.
3. State awal berbeda antara server dan client
Misalnya server memakai default filter all, tetapi client membaca preferensi dari localStorage lalu langsung memakai favorite pada render pertama. HTML server menampilkan semua item, sedangkan render awal browser hanya item favorit. Hasilnya: mismatch.
4. Iterasi atas struktur data yang urutannya tidak Anda kontrol
Jika Anda membangun UI dari hasil normalisasi object atau gabungan beberapa sumber data, jangan berasumsi bahwa urutan properti atau urutan input selalu identik di kedua sisi. Dalam praktiknya, masalah sering muncul saat data dikumpulkan bertahap, lalu diubah menjadi list tanpa normalisasi urutan yang eksplisit.
5. Komputasi non-deterministik di dalam render
Contoh umum:
Math.random()untuk memilih warna atau urutan item,Date.now()untuk label atau sorting,- pembacaan waktu lokal pengguna saat render awal,
- akses ukuran viewport, timezone, atau preferensi media saat SSR.
Semua itu dapat membuat output server berbeda dari output browser.
Contoh bug praktis pada Next.js, Nuxt, dan SvelteKit
Framework-nya berbeda, tetapi pola bug-nya sama: SSR dan client melakukan transformasi data dengan aturan yang tidak identik.
Contoh 1: Next.js – default state dari localStorage mengubah urutan list
function ProductList({ products }) {
const [sortMode] = React.useState(() => {
if (typeof window !== 'undefined') {
return window.localStorage.getItem('sortMode') || 'popular'
}
return 'popular'
})
const sorted = [...products].sort((a, b) => {
if (sortMode === 'price') return a.price - b.price
return b.popularity - a.popularity
})
return (
<ul>
{sorted.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
)
}Masalahnya: di server, sortMode selalu popular. Di browser, render pertama bisa langsung menjadi price. Urutan item berubah saat hydration.
Pendekatan yang lebih aman:
function ProductList({ products, initialSortMode = 'popular' }) {
const [sortMode, setSortMode] = React.useState(initialSortMode)
React.useEffect(() => {
const saved = window.localStorage.getItem('sortMode')
if (saved && saved !== sortMode) setSortMode(saved)
}, [sortMode])
const sorted = React.useMemo(() => {
return [...products].sort((a, b) => {
if (sortMode === 'price') return a.price - b.price
if (b.popularity !== a.popularity) return b.popularity - a.popularity
return a.id.localeCompare(b.id)
})
}, [products, sortMode])
return (
<ul>
{sorted.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
)
}Di sini, SSR dan render awal browser memakai nilai awal yang sama. Preferensi browser diterapkan setelah mount, bukan saat hydration pertama.
Contoh 2: Nuxt – computed list bergantung pada browser-only source
const sortMode = ref('latest')
if (process.client) {
sortMode.value = localStorage.getItem('sortMode') || 'latest'
}
const visiblePosts = computed(() => {
return [...posts.value].sort((a, b) => {
if (sortMode.value === 'popular') return b.views - a.views
return new Date(b.publishedAt) - new Date(a.publishedAt)
})
})Jika assignment ke sortMode memengaruhi render pertama di client tetapi tidak memengaruhi SSR, mismatch bisa terjadi. Solusinya tetap sama: samakan state awal, lalu sinkronkan preferensi client setelah mount, atau render bagian tertentu hanya di client bila memang harus bergantung pada API browser.
Contoh 3: SvelteKit – data dipetakan dengan transformasi non-deterministik
<script>
export let data;
const rows = data.items
.map(item => ({
...item,
score: Math.random()
}))
.sort((a, b) => b.score - a.score);
</script>
{#each rows as row (row.id)}
<div>{row.name}</div>
{/each}Server dan browser hampir pasti menghasilkan urutan berbeda karena memakai random saat render. Komputasi seperti ini harus dipindahkan ke server saja, diserialisasi sebagai data final, atau dijalankan setelah hydration bila memang hanya untuk efek client-side.
Langkah debugging yang efektif
Hydration mismatch dari urutan render sering sulit dilacak karena warning-nya muncul jauh dari sumber transformasi. Pendekatan berikut lebih efektif daripada menebak-nebak.
1. Bandingkan data input SSR vs render awal client
Log struktur data yang dipakai sebelum render list:
console.log('render products', products.map(p => p.id))Fokus pada:
- urutan id,
- nilai default filter/sort,
- hasil transformasi sebelum
mapke elemen UI.
Jika urutan id sudah berbeda, mismatch biasanya tinggal menunggu waktu.
2. Bekukan langkah transformasi satu per satu
Pisahkan pipeline data:
const normalized = normalize(products)
const filtered = applyFilter(normalized, filter)
const sorted = applySort(filtered, sortMode)
const viewModel = toViewModel(sorted)Lalu verifikasi output tiap tahap. Ini membantu menemukan tahap mana yang tidak deterministik.
3. Cari sumber non-deterministik di dalam render
Audit pemakaian:
window,document,localStorage,Date.now(),new Date(),Math.random(),- locale atau timezone yang tidak disamakan,
- akses viewport, media query, atau preferensi pengguna.
Bila ada di jalur render awal, curigai sebagai sumber mismatch.
4. Periksa key pada list
Gunakan key yang stabil dan unik. Hindari index sebagai key untuk list yang bisa berubah urutan. Index key tidak selalu menyebabkan mismatch langsung, tetapi memperparah patching DOM dan membuat gejala semakin sulit dipahami.
5. Gunakan view-source dan DOM setelah hydration
Lihat HTML yang benar-benar dikirim server, lalu bandingkan dengan DOM setelah browser selesai mount. Jika susunan item berubah tanpa interaksi pengguna, hampir pasti ada transformasi yang berbeda antara SSR dan client.
Teknik pencegahan yang praktis
Stabilkan sort dengan aturan lengkap
Jangan hanya mengembalikan 0 untuk banyak kasus tanpa tie-breaker. Buat comparator yang total dan eksplisit. Bila perlu, urutkan dengan beberapa level kriteria hingga jatuh ke ID unik.
Hindari akses browser API saat SSR dan render awal
Jangan membaca localStorage, ukuran layar, atau preferensi browser langsung untuk menentukan struktur HTML awal. Jika nilai itu penting, ada beberapa opsi:
- kirim nilai awal dari server bila memungkinkan,
- pakai default yang sama di server dan client,
- terapkan perubahan berbasis browser setelah mount,
- render komponen tertentu hanya di client jika SSR memang tidak relevan.
Samakan default state
Ini terlihat sepele, tetapi sangat sering menjadi akar masalah. Nilai awal untuk filter, sort, locale, tab aktif, dan status expanded harus identik pada SSR dan render awal browser.
Memoize transformasi data
Memoization bukan obat untuk mismatch, tetapi membantu menjaga pipeline data tetap konsisten dan mengurangi kerja ulang yang memperburuk UI tersendat.
const sortedItems = useMemo(() => {
return [...items].sort(compareItems)
}, [items, compareItems])Kenapa ini berguna? Karena transformasi kompleks tidak dijalankan ulang sembarangan pada re-render berikutnya. Ini membantu performa sekaligus memudahkan debugging, karena output lebih mudah diprediksi dari input yang sama.
Pisahkan komputasi non-deterministik dari render SSR
Jika Anda butuh randomisasi, waktu lokal pengguna, atau data browser-spesifik, lakukan salah satu:
- hitung di server dan kirim hasil final ke client,
- jalankan setelah hydration dalam effect/hook lifecycle,
- simpan hasilnya di state yang tidak memengaruhi struktur HTML awal.
Prinsipnya: render awal harus deterministik, interaktivitas boleh adaptif setelah mount.
Normalisasi data sebelum render
Bila data datang dari beberapa endpoint atau cache layer, normalisasi dulu:
- pastikan semua item punya ID stabil,
- ubah field yang nullable menjadi default yang konsisten,
- urutkan list secara eksplisit sebelum dikirim ke komponen.
Lebih baik mengirim view model yang sudah siap render daripada membiarkan tiap komponen melakukan transformasi sendiri-sendiri.
Trade-off yang perlu dipahami
Menunda logika client sampai setelah mount
Keuntungannya, hydration lebih aman. Kekurangannya, pengguna mungkin melihat perubahan UI kecil setelah mount, misalnya urutan list menyesuaikan preferensi tersimpan. Ini masih lebih baik daripada mismatch yang memicu re-render besar saat hydration.
Merender hanya di client
Ini menghindari mismatch pada komponen tertentu, tetapi mengurangi manfaat SSR: HTML awal lebih minim, SEO bisa terdampak untuk konten tertentu, dan waktu menuju konten interaktif bisa lebih lambat. Pilih ini hanya untuk bagian yang memang sangat bergantung pada browser.
Precompute data di server
Ini membuat output lebih stabil, tetapi memindahkan beban komputasi ke server dan menambah tanggung jawab pada layer data. Cocok untuk list yang penting bagi SEO atau pengalaman baca awal.
Pola implementasi yang aman
Bangun pipeline data yang deterministik
function buildProductView(products, options) {
const normalized = products.map((p) => ({
id: String(p.id),
name: p.name ?? '',
price: Number(p.price ?? 0),
featured: Boolean(p.featured),
popularity: Number(p.popularity ?? 0)
}))
const filtered = options.onlyFeatured
? normalized.filter((p) => p.featured)
: normalized
const sorted = [...filtered].sort((a, b) => {
if (options.sortMode === 'price') {
if (a.price !== b.price) return a.price - b.price
return a.id.localeCompare(b.id)
}
if (a.featured !== b.featured) return a.featured ? -1 : 1
if (b.popularity !== a.popularity) return b.popularity - a.popularity
return a.id.localeCompare(b.id)
})
return sorted
}Pola ini bekerja karena:
- input dinormalisasi dulu,
- filter dan sort dipisah jelas,
- sort punya tie-breaker stabil,
- fungsi tidak bergantung pada runtime browser.
Checklist singkat pencegahan hydration mismatch
- Apakah SSR dan render awal client memakai default state yang sama?
- Apakah semua sort punya aturan lengkap dan tie-breaker stabil?
- Apakah ada browser API yang dibaca untuk menentukan HTML awal?
- Apakah ada Math.random(), Date.now(), atau waktu lokal di jalur render?
- Apakah urutan list dibangun dari data yang sudah dinormalisasi?
- Apakah key list stabil dan bukan index?
- Apakah transformasi berat sudah memoized atau dipindah ke layer data?
- Apakah komputasi non-deterministik sudah dipisahkan dari SSR?
Penutup
Hydration mismatch dari urutan render adalah bug yang sering terlihat seperti masalah performa biasa. Padahal, akar persoalannya adalah ketidakkonsistenan output antara server dan browser. Begitu urutan pembacaan state, iterasi list, sort, atau transformasi data tidak stabil, HTML hasil SSR mudah berbeda dari render awal client.
Solusi praktisnya bukan sekadar “hindari window saat SSR”, tetapi memastikan seluruh pipeline render awal bersifat deterministik: state awal sama, sort stabil, akses data konsisten, transformasi dimemoize bila perlu, dan komputasi non-deterministik dipisahkan dari jalur SSR. Dengan begitu, hydration berjalan mulus dan UI tidak terasa tersendat pada saat yang paling penting: interaksi pertama pengguna.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!