Audit hydration SSR diperlukan ketika aplikasi server-side rendering menghasilkan HTML yang tampak benar di server, tetapi saat di-hydrate di browser justru memunculkan warning, layout melompat, event handler gagal menempel, atau sebagian UI berubah tiba-tiba. Akar masalahnya sederhana: render pertama di server harus menghasilkan struktur dan nilai yang sama dengan render pertama di browser.
Dalam praktiknya, syarat ini sering dilanggar oleh hal-hal yang tampak sepele: akses window atau localStorage terlalu dini, pemakaian Date atau Math.random() di render, perbedaan locale dan timezone, state awal yang berubah saat bootstrap, feature flag yang tidak sinkron, atau conditional rendering berbasis environment. Pelajaran umumnya sama seperti saat membaca perbedaan antara standar dan implementasi: jangan mengasumsikan runtime selalu berperilaku identik.
Apa itu hydration mismatch dan kenapa berbahaya?
Pada SSR, server mengirim HTML awal agar halaman bisa tampil cepat dan ramah SEO. Setelah itu, framework di browser melakukan hydration: ia mencocokkan virtual tree komponen dengan DOM yang sudah ada, lalu memasang event listener dan state reaktif.
Masalah terjadi jika output server dan output render awal di browser berbeda. Dampaknya bisa berupa:
- warning seperti text content does not match atau hydration failed,
- UI berkedip atau berubah setelah JavaScript aktif,
- state awal tidak konsisten,
- event listener menempel ke node yang salah,
- fallback ke client re-render penuh yang merusak manfaat SSR.
Hydration mismatch bukan sekadar warning kosmetik. Ia menandakan kontrak dasar SSR dilanggar: server dan browser tidak sepakat tentang hasil render pertama.
Prinsip audit: bandingkan input render, bukan hanya output
Banyak tim mencoba memperbaiki mismatch dengan menambal komponen yang terlihat rusak. Pendekatan ini sering tidak tahan lama. Audit yang benar dimulai dari pertanyaan:
- Apa saja input yang dipakai saat server merender komponen?
- Apa saja input yang dipakai saat browser merender komponen yang sama untuk pertama kali?
- Apakah input tersebut deterministik dan identik?
Input render bisa berupa props, data API, cookie, header, locale, timezone, feature flag, state store, nilai acak, waktu saat ini, ukuran viewport, preferensi user, dan kemampuan browser. Jika salah satu input berbeda, output render berpotensi berbeda juga.
Aturan praktis: logika yang mempengaruhi markup awal harus berbasis data yang tersedia dan konsisten di kedua sisi. Jika data hanya tersedia di browser, tunda penggunaannya sampai setelah hydration.
Sumber bug paling umum yang membuat server dan browser berbeda
1. Akses window, document, atau localStorage terlalu dini
Kesalahan klasik adalah membaca API browser langsung saat render. Di server, objek itu tidak ada. Bahkan jika dibungkus pengecekan environment, hasil render masih bisa berbeda karena browser memiliki data yang server tidak tahu.
// Buruk: hasil render awal bisa berbeda antara server dan browser
function ThemeLabel() {
const theme = localStorage.getItem('theme') || 'light'
return <span>Tema: {theme}</span>
}Jika server merender light tetapi browser menemukan dark di localStorage, maka teks awal berbeda.
Pola yang lebih aman adalah memakai nilai awal deterministik saat SSR, lalu membaca storage setelah komponen aktif di browser.
// Konseptual React/Next.js
function ThemeLabel() {
const [theme, setTheme] = useState('light')
useEffect(() => {
const saved = window.localStorage.getItem('theme')
if (saved) setTheme(saved)
}, [])
return <span>Tema: {theme}</span>
}Trade-off: pendekatan ini menghindari mismatch, tetapi mungkin menimbulkan perubahan UI kecil setelah mount. Jika itu mengganggu, kirim preferensi tema dari cookie ke server agar server dan browser memakai nilai awal yang sama.
2. Pemakaian Date, Math.random(), atau nilai non-deterministik saat render
Render SSR harus deterministik. Nilai waktu saat ini dan angka acak hampir pasti berbeda antara server dan browser.
// Buruk
export default function Banner() {
return <p>Dirender pada {new Date().toLocaleString()}</p>
}Bahkan selisih beberapa milidetik cukup untuk membuat teks berbeda. Masalah serupa muncul pada:
Math.random()untuk ID atau urutan item,Date.now()untuk label waktu,- UUID yang dibuat saat render,
- sorting yang melibatkan fungsi non-deterministik.
Perbaikannya:
- hasilkan nilai di server lalu kirim sebagai props jika memang harus tampil di HTML awal,
- atau hitung nilai tersebut hanya setelah hydration jika tidak penting untuk SSR,
- atau gunakan ID stabil yang berasal dari data, bukan dari random saat render.
3. Perbedaan locale dan timezone
Masalah ini sering tersembunyi. Server bisa berjalan dengan locale atau timezone yang berbeda dari browser pengguna. Akibatnya, format tanggal, angka, mata uang, bahkan aturan pluralisasi bisa berbeda.
// Berisiko bila locale/timezone tidak disamakan
const text = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(timestamp))Jika server memakai timezone UTC dan browser memakai Asia/Jakarta, hasil format bisa berbeda walau timestamp-nya sama.
Strategi yang lebih aman:
- format di server dengan locale dan timezone yang eksplisit bila hasilnya harus masuk ke HTML awal,
- atau tampilkan format netral saat SSR lalu lokalkan setelah mount,
- atau kirim locale/timezone pengguna ke server melalui cookie atau preferensi yang tersimpan.
Intinya bukan menghindari API format, tetapi memastikan input dan konfigurasi format sama di kedua runtime.
4. State awal berubah saat bootstrap
Hydration mismatch sering berasal dari store global atau state awal yang dihitung berbeda. Contohnya:
- store diinisialisasi dari cookie di server, tetapi dari
localStoragedi browser, - default state dihitung dari kondisi environment,
- data async tersedia saat SSR tetapi tertimpa state kosong di client sebelum hydration selesai.
Gejala umum: server menampilkan user sudah login, tetapi browser merender state guest pada pass pertama; atau server menampilkan item keranjang 3, browser merender 0.
Solusinya adalah menyamakan sumber kebenaran untuk state awal. Jika server merender berdasarkan sesi atau cookie, browser harus menerima snapshot state yang sama saat bootstrap, bukan menghitung ulang dari sumber lain.
5. Feature flag yang tidak sinkron
Feature flag menambah dimensi baru pada SSR: nilai flag harus konsisten antara server dan browser. Jika server memakai cache konfigurasi lama atau browser mengambil flag terbaru dari SDK client sebelum hydration, UI bisa berbeda.
Contoh masalah:
- server menampilkan tombol baru karena flag aktif, browser masih menganggap flag mati,
- server memakai segmentasi berdasarkan cookie, browser memakai identitas anonim sementara,
- evaluasi flag bergantung pada data yang belum tersedia saat render server.
Pola aman:
- evaluasi flag di server untuk initial render,
- serialisasikan hasil evaluasi itu ke halaman,
- gunakan snapshot yang sama di browser untuk render pertama,
- baru sinkronkan ulang setelah hydration bila diperlukan.
6. Conditional rendering berbasis environment
Pengecekan seperti typeof window !== 'undefined' memang mencegah error di server, tetapi tidak otomatis mencegah mismatch.
// Buruk: markup awal bisa berbeda
function Sidebar() {
if (typeof window === 'undefined') {
return null
}
return <aside>Sidebar interaktif</aside>
}Server mengembalikan null, browser mengembalikan <aside> pada render pertama. Ini mismatch.
Pola yang lebih aman adalah membuat render awal sama, lalu mengaktifkan bagian browser-only setelah mount:
// Konseptual React/Next.js
function Sidebar() {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return <aside aria-hidden="true" />
}
return <aside>Sidebar interaktif</aside>
}Trade-off-nya: Anda menunda sebagian UI. Karena itu, gunakan hanya untuk bagian yang benar-benar bergantung pada API browser.
Contoh audit secara konseptual di Next.js, Nuxt, dan SvelteKit
Walau detail API tiap framework berbeda, pola diagnosisnya hampir sama.
Next.js
Di ekosistem React, perhatikan komponen yang:
- mengakses browser API di body komponen,
- membangun state awal dari
windowatau storage, - mencetak tanggal atau angka acak langsung di JSX,
- merender subtree berbeda berdasarkan environment.
Untuk bagian yang tidak layak dirender di server, gunakan strategi client-only secara sadar, bukan sebagai default. Jika seluruh komponen tergantung browser, menonaktifkan SSR untuk komponen itu bisa lebih tepat daripada memaksa markup semu yang rapuh.
Nuxt
Di ekosistem Vue/Nuxt, cek apakah data awal datang dari sumber yang sama pada SSR dan client hydration. Hati-hati dengan komputasi di template atau setup yang menyentuh waktu, locale, atau objek browser. Jika ada komponen yang memang hanya relevan di browser, tempatkan dalam boundary client-only yang sesuai.
SvelteKit
Di SvelteKit, sumber mismatch sering muncul dari kode komponen yang membaca environment browser saat pembuatan markup, atau dari data load yang tidak diserialisasikan secara konsisten ke client. Pisahkan logika yang boleh jalan di server dan yang harus menunggu browser aktif.
Pelajaran lintas framework: nama API boleh berbeda, tetapi model mentalnya sama. SSR yang sehat bergantung pada render awal yang deterministik dan data bootstrap yang konsisten.
Checklist debugging langkah demi langkah
Berikut checklist praktis untuk mengaudit hydration mismatch tanpa menebak-nebak.
1. Reproduksi dengan input yang stabil
- Gunakan satu URL spesifik dan satu akun uji.
- Matikan eksperimen acak bila memungkinkan.
- Uji di mode produksi lokal, karena perilaku dev dan prod bisa berbeda.
- Catat locale, timezone, cookie, dan status login saat reproduksi.
2. Baca pesan mismatch, tetapi jangan berhenti di sana
Framework biasanya memberi petunjuk node mana yang berbeda: teks, atribut, atau struktur. Gunakan itu sebagai titik awal, bukan diagnosis final. Node yang dilaporkan sering hanya korban; penyebab aslinya bisa lebih tinggi di tree.
3. Bandingkan HTML server dengan render awal browser
Lihat source HTML hasil SSR, lalu bandingkan dengan DOM setelah JavaScript berjalan sangat awal. Fokus pada area yang bermasalah:
- apakah teks berubah,
- apakah jumlah elemen berbeda,
- apakah atribut kelas atau style berubah,
- apakah urutan item list berpindah.
Jika output berbeda, tanyakan input mana yang memengaruhi bagian itu.
4. Cari semua sumber non-deterministik dalam subtree
Lakukan pencarian cepat pada komponen terkait untuk pola berikut:
window,document,navigator,matchMedia,localStorage,sessionStorage,Date,Date.now,new Intl,Math.random, UUID, sorting acak,- cabang
ifberbasis environment, - state awal dari store atau singleton global.
5. Instrumentasi nilai input render
Tambahkan logging sementara untuk nilai yang dipakai saat render pertama, baik di server maupun di browser. Jangan hanya log hasil akhir; log input yang membentuk hasil.
// Pseudocode umum
log('render-input', {
locale,
timezone,
userId,
featureFlags,
initialTheme,
nowUsedInRender,
hasWindow: typeof window !== 'undefined'
})Tujuannya adalah menemukan variabel pertama yang tidak sama antara dua runtime.
6. Isolasi komponen secara bertahap
Jika halaman kompleks, jangan debug seluruh tree sekaligus. Isolasi subtree yang dicurigai:
- nonaktifkan sementara komponen anak,
- ganti dengan placeholder statis,
- aktifkan satu per satu sampai mismatch kembali muncul.
Teknik ini sangat efektif ketika masalah berasal dari satu widget kecil yang mempengaruhi node lebih besar.
7. Periksa serialisasi data SSR
Pastikan data yang dipakai server benar-benar dikirim ke browser tanpa perubahan makna. Waspadai:
Dateyang berubah jadi string lalu diformat berbeda,- nilai
undefinedyang hilang saat serialisasi, - objek
Map,Set, atau class instance yang tidak pulih identik, - angka, locale, atau boolean yang berubah karena parsing.
8. Uji dengan timezone dan locale berbeda
Jika mismatch tampak acak atau hanya terjadi pada sebagian pengguna, curigai locale/timezone. Uji setidaknya dengan:
- satu timezone UTC,
- satu timezone lokal tim Anda,
- satu locale berbeda dari default server.
9. Verifikasi bootstrap feature flag dan auth state
Pastikan browser tidak melakukan evaluasi ulang yang berbeda sebelum render pertama selesai. Snapshot awal harus sama dengan yang dipakai server.
Strategi perbaikan yang paling aman
Gunakan nilai awal yang deterministik
Nilai awal untuk render SSR sebaiknya dapat dihitung identik di server dan browser. Jika tidak mungkin, gunakan placeholder netral yang sama di kedua sisi, lalu perbarui setelah mount.
Pindahkan efek browser-only ke fase setelah mount
Hal-hal seperti membaca storage, ukuran viewport, preferensi media, atau API perangkat sebaiknya tidak mempengaruhi markup awal. Jalankan setelah hydration, lalu update state secara terkontrol.
Serialisasikan snapshot state dari server
Untuk auth state, cart, preferensi, atau feature flag, lebih aman mengirim snapshot state dari server ke client daripada menghitung ulang dari nol. Ini menjaga render pertama tetap konsisten.
Tentukan locale dan timezone secara eksplisit
Jika format tanggal/angka ada di HTML awal, jangan bergantung pada default runtime. Gunakan locale dan timezone yang diketahui, atau tampilkan format yang tidak ambigu hingga browser siap melakukan lokalisasi.
Hindari ID atau key yang dibuat acak saat render
Gunakan identifier stabil dari data. Key list yang tidak stabil bukan hanya memicu mismatch, tetapi juga menyebabkan bug state komponen yang sulit dilacak.
Kapan memakai client-only rendering?
Tidak semua komponen layak dirender di server. Ada kasus di mana client-only rendering adalah keputusan yang benar, misalnya:
- komponen sangat bergantung pada API browser seperti geolocation, media devices, canvas interaktif, editor WYSIWYG, atau visualisasi yang membutuhkan ukuran viewport nyata,
- nilai awal sepenuhnya personal dan hanya tersedia di browser,
- biaya menjaga SSR konsisten lebih besar daripada manfaat SEO atau performa untuk komponen itu.
Namun jangan jadikan client-only sebagai solusi otomatis untuk semua mismatch. Trade-off-nya jelas:
- HTML awal berkurang atau hilang untuk komponen tersebut,
- time-to-interactive bagian itu bergantung pada JavaScript client,
- SEO dan perceived performance bisa menurun jika diterapkan pada area penting halaman.
Pilih client-only untuk komponen yang memang tidak punya manfaat kuat dari SSR. Untuk konten utama halaman, prioritaskan perbaikan determinisme render.
Pola pencegahan untuk tim dan code review
Buat aturan: render harus murni dan deterministik
Dalam code review, anggap fungsi render seperti fungsi murni: dengan input yang sama, hasilnya harus sama. Pertanyakan semua akses ke waktu, random, storage, dan environment.
Pisahkan data SSR dan data browser-only
Dokumentasikan mana data yang boleh mempengaruhi HTML awal, dan mana yang baru boleh dipakai setelah mount. Batas ini mencegah banyak bug desain sejak awal.
Tambahkan pengujian untuk skenario sensitif
Setidaknya uji:
- halaman dengan user login vs anonim,
- theme tersimpan vs default,
- locale berbeda,
- timezone berbeda,
- flag aktif vs nonaktif.
Tujuannya bukan mengejar cakupan berlebihan, melainkan menangkap perbedaan runtime yang paling sering lolos.
Waspadai asumsi tentang standar vs implementasi
Secara teori, API JavaScript yang sama tersedia di banyak environment. Dalam kenyataan, default locale, timezone, urutan evaluasi data, waktu bootstrap, dan sumber konfigurasi bisa berbeda. Inilah pelajaran penting dari topik standar vs implementasi: kesamaan API tidak menjamin kesamaan hasil.
Contoh matriks diagnosis cepat
- Teks tanggal berbeda → periksa
Date,Intl, locale, timezone. - Theme atau auth berubah setelah load → periksa storage, cookie, dan snapshot state awal.
- Komponen hilang di server tapi muncul di browser → periksa conditional rendering berbasis environment.
- Urutan list berubah → periksa sort non-deterministik, key tidak stabil, random.
- UI berbeda antar segmen user → periksa feature flag dan sumber identitas saat evaluasi awal.
Penutup
Audit hydration SSR pada dasarnya adalah audit determinisme. Ketika UI berbeda antara server dan browser, penyebabnya hampir selalu karena input render awal tidak identik, atau karena logika render menyentuh hal yang tidak stabil antar runtime. Fokuslah pada sumber perbedaan: browser API, waktu, random, locale/timezone, state bootstrap, feature flag, dan conditional environment.
Jika Anda menerapkan satu aturan utama, pilih ini: markup awal harus lahir dari data yang sama dan aturan yang sama di kedua sisi. Sisanya—strategi isolasi komponen, snapshot state, penundaan efek browser-only, atau client-only rendering—adalah alat untuk menjaga aturan itu tetap benar dalam aplikasi SSR modern.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!