Debug state hydration yang memicu UI flicker di SSR dimulai dari satu prinsip penting: server dan client harus merender output awal yang sama. Jika HTML yang dikirim server menampilkan satu state, lalu sesaat setelah JavaScript client aktif state berubah menjadi nilai lain, pengguna akan melihat gejala seperti tema berkedip, tombol login berubah menjadi profil, teks berganti, atau komponen kondisional muncul-hilang.
Masalah ini umum pada aplikasi SSR modern karena data awal sering datang dari dua sumber berbeda: server memakai cookie atau request header, sedangkan client membaca localStorage, window, atau hasil fetch yang datang belakangan. Akibatnya, hydration bukan hanya “menempelkan event listener”, tetapi juga memicu render ulang dengan state baru. Artikel ini fokus pada diagnosis sistematis dan perbaikannya secara framework-agnostic, sambil tetap relevan untuk Next.js, Nuxt, dan SvelteKit.
Memahami gejala UI flicker saat hydration
UI flicker adalah perubahan visual singkat antara hasil render SSR dan hasil render pertama di browser setelah client mount atau hydration berjalan. Gejalanya sering terlihat kecil, tetapi akar masalahnya penting karena menyentuh konsistensi state.
Contoh gejala yang paling sering muncul
- Tema: server merender mode terang, lalu client membaca preferensi gelap dari
localStoragedan tampilan berubah sesaat. - Status autentikasi: server belum mengenali user, lalu client menemukan token lokal dan navbar berubah dari “Masuk” menjadi avatar pengguna.
- Teks atau label: placeholder awal berubah setelah mount karena state default di client berbeda.
- Tombol atau aksi: tombol Follow berubah menjadi Following atau Add to cart berubah karena data lokal masuk terlambat.
- Komponen kondisional: modal, banner, skeleton, atau elemen berbasis role muncul lalu hilang.
Mengapa ini bukan sekadar masalah kosmetik
Flicker memengaruhi persepsi kualitas, dapat menurunkan kepercayaan pengguna, dan sering menandakan mismatch data yang lebih dalam. Dalam beberapa kasus, mismatch hydration juga memunculkan warning di console atau menyebabkan event binding terjadi pada struktur DOM yang berbeda dari perkiraan framework.
Akar masalah utama state hydration
Untuk memperbaiki flicker, Anda perlu mengidentifikasi sumber state pertama yang dipakai server dan client. Hampir semua kasus jatuh ke salah satu kategori berikut.
1. Sumber data server dan client berbeda
Ini penyebab paling umum. Misalnya, server menentukan bahwa pengguna belum login karena hanya melihat cookie tertentu, tetapi client memakai token dari localStorage. Atau server merender locale default, sementara client membaca locale terakhir dari penyimpanan browser.
Jika sumber state tidak sama, maka output HTML awal hampir pasti berbeda.
2. Akses window atau localStorage terlalu dini
Pada SSR, objek browser seperti window, document, matchMedia, dan localStorage tidak tersedia saat render di server. Pola yang salah sering berupa inisialisasi state langsung dari API browser dalam body komponen atau saat pembuatan store global tanpa fallback yang konsisten.
Akibatnya, server memakai nilai default A, tetapi client segera menghitung nilai B setelah mount.
3. Default state server dan client tidak identik
Walaupun tidak mengakses API browser, flicker bisa tetap muncul jika nilai default berbeda. Misalnya, server merender isOpen = false, sedangkan client menginisialisasi store dari cache atau props yang tidak sinkron menjadi true.
4. Render yang tidak deterministik
Render harus deterministik untuk input yang sama. Jika Anda memakai Date.now(), Math.random(), timezone lokal browser, urutan object yang tidak stabil, atau ID acak saat render awal, maka server dan client bisa menghasilkan markup berbeda.
Masalah ini juga sering muncul pada format tanggal, angka, atau locale jika server dan browser memiliki konteks berbeda.
5. Data async datang setelah hydration
Server mungkin merender dengan data kosong atau placeholder, lalu client langsung melakukan fetch dan mengganti UI. Ini tidak selalu salah, tetapi jika data tersebut sebenarnya bisa diketahui saat SSR, maka hasilnya terasa seperti flicker yang tidak perlu.
Langkah debug sistematis
Alih-alih menebak-nebak, lakukan inspeksi terstruktur. Tujuannya adalah membuktikan kapan state berubah dan dari mana nilainya datang.
1. Identifikasi elemen yang berubah
Mulai dari gejala yang terlihat. Tanyakan:
- Elemen mana yang berkedip: teks, kelas CSS, atribut, atau seluruh komponen?
- Perubahan terjadi sebelum interaksi pengguna atau setelah mount?
- Apakah hanya terjadi pada refresh penuh, hard reload, atau juga saat navigasi client-side?
Catat perubahan sekecil apa pun. Misalnya, body class berubah dari theme-light ke theme-dark, atau tombol berubah label dari Login ke Dashboard.
2. Bandingkan output SSR dengan render client pertama
Buka halaman dengan JavaScript aktif dan periksa HTML awal secepat mungkin. Jika perlu:
- Lihat source HTML dari server.
- Bandingkan dengan DOM setelah hydration menggunakan DevTools.
- Perhatikan node, class, atribut
data-*, dan teks yang berubah.
Jika source HTML sudah berbeda dari DOM sesaat setelah mount, maka Anda sedang melihat mismatch state, bukan sekadar transisi CSS.
3. Log asal state, bukan hanya nilainya
Tambahkan logging sementara di sisi server dan client. Yang penting bukan hanya theme=dark, tetapi juga kenapa nilainya dark.
// Pseudocode framework-agnostic
// Server
console.log('[server] theme from cookie:', cookieTheme)
console.log('[server] auth from request session:', !!sessionUser)
// Client
console.log('[client] theme from localStorage:', localStorage.getItem('theme'))
console.log('[client] auth from token cache:', !!token)
Dengan cara ini Anda bisa langsung melihat ketidaksesuaian sumber data.
4. Tinjau lokasi inisialisasi state
Periksa apakah state kritis diinisialisasi:
- di render awal komponen,
- di store global yang dieksekusi pada import,
- di hook lifecycle setelah mount,
- atau di loader/fetch SSR.
Banyak flicker berasal dari store yang membaca localStorage saat file diimport di client, tetapi server memakai default state lain.
5. Cari render non-deterministik
Lakukan pencarian kode untuk pola berikut:
Date.now()new Date()yang diformat langsung saat renderMath.random()- ID unik yang dibuat di render
- akses timezone atau locale browser saat SSR tidak punya konteks yang sama
Jika nilai ini dipakai untuk menentukan isi atau struktur UI awal, hasil SSR dan client bisa berbeda.
6. Gunakan throttling untuk memperjelas gejala
Network throttling dan CPU slowdown di DevTools membantu memperlihatkan urutan perubahan. Flicker yang sulit dilihat pada mesin cepat sering menjadi jelas saat hydration melambat beberapa ratus milidetik.
Checklist inspeksi saat debug hydration mismatch
Gunakan daftar ini sebagai audit cepat:
- Apakah state awal di server dan client berasal dari sumber yang sama?
- Apakah ada pembacaan
window,document,matchMedia, ataulocalStoragesebelum mount? - Apakah default state di server sama persis dengan default state di client?
- Apakah data auth berasal dari cookie/session di server, tetapi token lokal di client?
- Apakah tema ditentukan oleh cookie, class HTML awal, atau baru dibaca dari storage setelah mount?
- Apakah ada nilai acak, timestamp, atau format locale yang dihitung saat render?
- Apakah komponen kondisional dirender berdasarkan state yang baru tersedia di browser?
- Apakah fetch di client mengambil data yang sebenarnya sudah bisa disediakan saat SSR?
- Apakah ada warning hydration mismatch di console?
- Apakah flicker sebenarnya berasal dari CSS atau transisi, bukan dari state?
Pola yang salah vs pola yang benar
Contoh berikut bersifat umum dan bisa diterapkan lintas framework.
Pola salah: membaca storage saat menentukan UI awal
// Salah: server tidak punya localStorage, client punya.
function getInitialTheme() {
return localStorage.getItem('theme') || 'light'
}
const theme = getInitialTheme()
renderTheme(theme)
Masalahnya, server akan merender fallback light atau bahkan gagal jika tidak ada guard. Setelah hydration, client membaca dark dan UI berkedip.
Pola benar: samakan sumber state awal, lalu sinkronkan
// Ide yang benar:
// 1. Server menentukan theme awal dari cookie/request.
// 2. HTML awal memakai nilai itu.
// 3. Client mengadopsi nilai yang sama saat hydration.
// 4. localStorage hanya dipakai untuk sinkronisasi lanjutan, bukan sumber kebenaran awal.
const initialTheme = serverProvidedTheme // mis. dari cookie
const theme = createState(initialTheme)
onClientMounted(() => {
const saved = localStorage.getItem('theme')
if (saved && saved !== theme.value) {
theme.value = saved
}
})
Pola ini tetap bisa menimbulkan perubahan jika cookie dan storage berbeda, tetapi Anda sekarang punya strategi yang jelas: tentukan satu source of truth untuk render awal. Dalam praktiknya, untuk SSR biasanya cookie atau data request lebih aman daripada storage browser.
Pola salah: auth hanya diketahui di client
// Salah: server selalu anggap user anonymous
const isAuthenticated = !!localStorage.getItem('token')
Hasilnya, navbar SSR menampilkan tombol login, lalu berubah setelah mount.
Pola benar: auth awal diturunkan dari request server
// Benar secara arsitektur:
// server membaca session/cookie lalu mengirim state auth awal
const initialAuth = serverProvidedAuth
const authState = createState(initialAuth)
onClientMounted(() => {
// optional revalidation jika memang perlu
revalidateAuthInBackground()
})
Jika aplikasi Anda memang memakai token browser-only, pertimbangkan untuk tidak merender bagian auth-sensitif pada SSR, atau tampilkan placeholder netral sampai status pasti tersedia.
Pola salah: render kondisional bergantung pada browser API
// Salah
const isMobile = window.innerWidth < 768
return isMobile ? renderCompactMenu() : renderFullMenu()
Lebar viewport tidak tersedia di server dengan cara yang setara. Hasilnya bisa berbeda saat hydration.
Pola benar: gunakan CSS responsif atau defer keputusan ke client-only section
Untuk layout responsif, prioritaskan CSS. Jika benar-benar perlu keputusan berbasis browser runtime, render fallback netral atau bungkus sebagai komponen client-only.
Strategi perbaikan yang efektif
1. Tetapkan satu source of truth untuk render awal
Jika state memengaruhi HTML pertama, sumber nilainya harus dapat diakses server dan client secara konsisten. Pilihan yang umum:
- Cookie untuk tema, locale, preferensi sederhana.
- Session/request context untuk auth, role, eksperimen, atau data user.
- Server-fetched data untuk konten yang memang dibutuhkan sebelum halaman ditampilkan.
Hindari menjadikan localStorage sebagai sumber kebenaran untuk state yang menentukan output SSR awal.
2. Pisahkan state awal dari sinkronisasi client
Bedakan dua fase:
- Initial render state: harus konsisten antara server dan client.
- Client reconciliation: pembaruan setelah mount jika ada informasi tambahan dari browser.
Dengan pemisahan ini, Anda bisa mengurangi perubahan visual mendadak dan memahami apakah update setelah mount memang diperlukan.
3. Render placeholder yang stabil untuk state yang belum pasti
Jika informasi memang hanya tersedia di browser, jangan pura-pura tahu saat SSR. Lebih aman merender placeholder netral daripada merender state yang salah lalu menggantinya.
Contoh yang baik:
- menampilkan ikon tema netral sampai preferensi diketahui,
- menyembunyikan menu user sensitif sampai status auth tervalidasi,
- menampilkan skeleton kecil yang stabil secara layout.
Trade-off-nya adalah informasi tampil sedikit lebih lambat, tetapi pengguna tidak melihat pergantian yang membingungkan.
4. Hindari render non-deterministik
Jangan hitung nilai acak, timestamp saat ini, atau format lokal browser di render awal jika hasilnya memengaruhi markup. Jika butuh ID unik, gunakan mekanisme yang stabil. Jika perlu waktu saat ini, kirim nilainya dari server atau render setelah mount dengan placeholder yang aman.
5. Pindahkan logika browser-only ke lifecycle client
Akses window, matchMedia, observer, ukuran viewport, dan storage sebaiknya dilakukan setelah mount atau pada blok yang memang hanya berjalan di browser. Namun ingat: ini menghindari crash di server, bukan otomatis menghilangkan flicker. Anda tetap perlu memastikan HTML awal tidak menampilkan state yang nantinya berubah drastis.
6. Preload state penting ke HTML secara eksplisit
Untuk beberapa kasus, server dapat menyisipkan state awal ke dalam HTML agar client melakukan hydration dengan nilai yang sama. Pendekatan ini umum untuk data user, tema, atau preferensi. Pastikan mekanisme serialisasi aman dan tidak membocorkan data sensitif.
Catatan: Jangan menyuntikkan token rahasia atau data sensitif mentah ke HTML hanya demi menghindari flicker. Untuk auth, kirim status atau profil minimum yang memang aman dirender, bukan kredensial.
Relevansi untuk Next.js, Nuxt, dan SvelteKit
Walaupun detail API tiap framework berbeda, pola diagnosis dan perbaikannya sama.
Next.js
Pastikan data yang menentukan UI awal datang dari konteks request atau mekanisme data fetching server, bukan baru dibaca di komponen client dari storage. Untuk tema dan auth, samakan nilai yang dipakai server dan nilai yang dipakai saat client pertama kali merender. Jika ada bagian yang benar-benar hanya masuk akal di browser, pertimbangkan komponen client-only secara selektif.
Nuxt
Gunakan state yang dapat diinisialisasi dari server dan diwariskan ke client. Hati-hati dengan plugin atau composable yang membaca browser API terlalu awal. Jika tema atau auth disimpan di cookie, gunakan itu sebagai basis render SSR, lalu sinkronkan ke state client setelah mount bila perlu.
SvelteKit
Tempatkan data awal pada jalur load atau konteks server yang memang dirancang untuk SSR. Hindari store global yang langsung menyentuh browser API saat modul dievaluasi. Jika suatu komponen sangat bergantung pada runtime browser, lebih aman jadikan ia client-only daripada memaksa SSR lalu mengalami flicker.
Poin utamanya bukan memilih framework tertentu, melainkan menjaga agar nilai yang menentukan markup pertama selalu stabil dan konsisten.
Kapan client-only rendering lebih masuk akal
Tidak semua komponen harus dipaksa SSR. Gunakan client-only rendering jika:
- komponen sangat bergantung pada API browser, seperti ukuran viewport, sensor, editor kaya, peta, atau integrasi widget pihak ketiga;
- state awal tidak bisa diketahui secara andal di server;
- biaya menjaga parity SSR-client lebih besar daripada manfaat SEO atau perceived performance;
- komponen tidak kritis untuk konten awal halaman.
Namun, jangan menjadikan client-only sebagai solusi default untuk seluruh halaman. Anda akan kehilangan manfaat SSR untuk performa awal, SEO, dan stabilitas konten yang bisa dirender di server.
Panduan pencegahan agar UI tidak berkedip lagi
- Tentukan source of truth sejak awal untuk tema, auth, locale, dan preferensi UI penting.
- Gunakan cookie atau request-bound state untuk data yang memengaruhi SSR.
- Jangan membaca browser API saat render server atau saat inisialisasi state global tanpa guard yang jelas.
- Pastikan default state identik antara server dan client.
- Hindari render non-deterministik seperti waktu sekarang, random, dan format locale yang tidak sinkron.
- Beri placeholder yang stabil untuk state yang memang belum tersedia.
- Uji dengan hard reload dan throttling, bukan hanya navigasi client-side.
- Periksa warning hydration di console dan telusuri node yang berubah.
- Audit store, plugin, dan helper yang dieksekusi saat import modul.
- Dokumentasikan kontrak data awal antara server dan client agar tim tidak membuat asumsi berbeda.
Penutup
UI flicker pada SSR hampir selalu berasal dari satu hal: state awal di server dan client tidak benar-benar sama. Cara memperbaikinya bukan dengan menambah patch visual semata, tetapi dengan menertibkan sumber data, waktu inisialisasi, dan determinisme render. Saat Anda melihat teks, tombol, tema, status auth, atau komponen kondisional berubah sesaat setelah mount, fokuslah pada pertanyaan inti: nilai pertama ini datang dari mana di server, dan dari mana di client?
Jika jawaban keduanya berbeda, Anda sudah menemukan akar masalah. Dari situ, pilih strategi yang tepat: samakan source of truth, pindahkan logika browser-only ke fase client, tampilkan placeholder netral, atau gunakan client-only rendering untuk komponen yang memang tidak cocok di-SSR-kan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!