Hydration drift akibat state lama yang tersisip di SSR biasanya muncul saat server mengirim HTML dan payload state yang sudah tidak lagi valid, lalu browser melakukan hydration dengan sumber data yang lebih baru atau berbeda. Hasilnya UI terlihat membingungkan: elemen sempat tampil benar lalu berubah, tombol seolah “kembali” ke status lama, atau framework mengeluarkan peringatan mismatch.

Masalah ini mirip dengan kasus perubahan yang sudah direvert tetapi “hidup lagi” karena ada snapshot lama yang masih ikut dipakai. Dalam konteks SSR, guardrail-nya adalah memastikan satu sumber kebenaran state benar-benar konsisten dari render server sampai hydration klien. Jika ada cache, embedded JSON, localStorage, cookie, feature flag, atau nilai berbasis waktu yang tidak sinkron, drift akan muncul.

Apa itu hydration drift, dan kenapa gejalanya sering menipu?

Pada aplikasi SSR, server merender HTML awal agar halaman cepat tampil dan mudah diindeks. Setelah itu, JavaScript di klien melakukan hydration: framework “menempelkan” event handler dan membangun ulang state komponen berdasarkan payload awal atau perhitungan ulang di browser.

Hydration drift terjadi saat state yang dipakai server untuk menghasilkan HTML tidak identik dengan state yang dipakai klien saat hydration. Ini bukan selalu bug render murni. Sering kali akar masalahnya ada pada alur data, bukan komponen UI itu sendiri.

Gejala yang umum terlihat

  • UI sempat menampilkan status A, lalu langsung berubah ke status B setelah hydration.
  • Peringatan seperti “hydration mismatch”, “text content does not match”, atau node dirender ulang penuh di klien.
  • Tombol, badge, harga, flag eksperimen, atau status login terlihat tidak konsisten.
  • Bug sulit direproduksi karena hanya muncul pada kombinasi cache tertentu, pengguna tertentu, atau waktu tertentu.

Contoh analogi: revert yang “hidup lagi”

Bayangkan sebuah perubahan state sudah dibatalkan, tetapi snapshot lama masih tersimpan di salah satu jalur distribusi. Saat request berikutnya datang, SSR masih menyisipkan snapshot lama itu ke HTML. Di sisi klien, kode membaca sumber yang lebih baru, lalu UI berubah sesaat setelah hydration. Pengguna melihat efek seolah perubahan lama muncul lagi. Ini bukan sekadar masalah visual; ini tanda bahwa guardrail state Anda bocor.

Sumber mismatch paling sering: dari mana state lama masuk?

Untuk mendiagnosis hydration drift, fokuslah pada semua tempat yang bisa menyuplai state awal. Banyak tim hanya memeriksa komponen, padahal masalahnya sering berasal dari jalur data paralel.

1. Cache SSR atau cache di edge/CDN

Jika HTML atau payload data di-cache terlalu lama, server bisa mengirim snapshot lama meskipun data backend sudah berubah. Ini umum pada halaman yang memuat status pengguna, feature flag, atau data yang sensitif terhadap sesi.

  • HTML di-cache tetapi API klien tidak di-cache, sehingga klien mendapatkan data baru saat hydration.
  • Cache key tidak memasukkan dimensi penting seperti locale, cookie, header eksperimen, atau status autentikasi.
  • Invalidasi hanya dilakukan pada API, bukan pada HTML SSR atau data payload framework.

2. Embedded JSON di HTML

Banyak framework SSR menyisipkan state awal ke dalam <script> atau payload khusus. Jika payload ini dibentuk dari sumber yang berbeda dengan data yang dipakai klien setelah mount, drift mudah terjadi.

Contoh umum:

  • Server merender dari query database lama karena cache DAL belum invalid.
  • Klien langsung refetch ke endpoint baru yang mengembalikan data terbaru.
  • Normalisasi data di server dan klien berbeda, misalnya default value tidak sama.

3. localStorage atau sessionStorage

SSR tidak bisa membaca localStorage. Jika klien mengganti state awal dari browser storage begitu aplikasi berjalan, HTML hasil SSR dan state klien akan berbeda. Ini sering terjadi pada tema, keranjang belanja, preferensi filter, onboarding, dan flag lokal.

4. Cookie dan state berbasis sesi

Cookie dapat dibaca server dan klien, tetapi drift tetap bisa terjadi bila:

  • Server membaca cookie request lama, lalu klien membaca cookie yang sudah berubah.
  • Reverse proxy atau edge cache tidak memvariasikan cache berdasarkan cookie yang relevan.
  • State autentikasi di-refresh di klien setelah halaman diterima.

5. Feature flag dan eksperimen

Feature flag adalah sumber klasik hydration drift. Jika evaluasi flag di server dan klien menggunakan context berbeda, pengguna bisa menerima HTML untuk varian A, tetapi klien meng-hydrate sebagai varian B.

Penyebabnya bisa berupa:

  • Identifier pengguna belum tersedia saat SSR, tetapi tersedia saat klien berjalan.
  • SDK flag di klien melakukan fetch ulang dan mendapatkan konfigurasi lebih baru.
  • Flag dievaluasi di edge dengan aturan yang berbeda dari backend atau browser.

6. Waktu render, zona waktu, dan nilai nondeterministik

Nilai seperti Date.now(), new Date(), angka acak, format waktu lokal, atau urutan object yang tidak stabil dapat menghasilkan HTML berbeda dari hasil render klien. Ini bukan “state lama”, tetapi efeknya sama: server dan klien memulai dari snapshot yang tidak identik.

Prinsip penting: hydration yang sehat membutuhkan output awal yang deterministik. Jika server dan klien menghitung hal berbeda untuk state awal, mismatch bukan anomali—itu konsekuensi yang wajar.

Langkah debug terurut: jangan mulai dari komponen

Cara tercepat memperbaiki hydration drift adalah memeriksa rantai state dari server ke browser secara berurutan. Mulai dari output SSR, lalu payload, baru efek di klien.

1. Audit snapshot SSR yang benar-benar dikirim

Ambil HTML mentah dari respons server, bukan DOM setelah hydration. Periksa:

  • Nilai teks yang dirender server.
  • Payload state yang disisipkan ke HTML.
  • Header cache, cookie, dan variasi respons.

Tujuannya adalah menjawab pertanyaan sederhana: server sebenarnya mengirim state apa? Banyak bug selesai hanya dengan melihat bahwa HTML yang dikirim memang sudah salah sebelum JavaScript berjalan.

Contoh pola logging di server:

// Pseudocode di sisi SSR/server
const requestId = crypto.randomUUID()
const snapshot = {
  userId: user?.id ?? null,
  featureVariant,
  cartCount,
  source: {
    cart: 'redis-cache',
    featureFlag: 'flag-service',
  },
  generatedAt: new Date().toISOString()
}

logger.info('ssr_snapshot', {
  requestId,
  route: req.url,
  snapshot
})

// Sisipkan requestId ke HTML untuk korelasi client-server

Jangan log data sensitif mentah. Log hanya metadata yang cukup untuk korelasi dan diagnosis.

2. Diff payload SSR dengan state awal di klien

Setelah tahu snapshot SSR, bandingkan dengan state yang dipakai klien saat hydration. Fokus pada nilai yang memengaruhi struktur atau teks UI.

Yang perlu dicatat:

  • Apakah klien memakai embedded JSON yang sama dengan server?
  • Apakah ada override dari localStorage, cookie, atau refetch langsung saat mount?
  • Apakah serializer/deserializer mengubah bentuk data?

Contoh logging hydration di klien:

// Pseudocode di klien
const ssrPayload = window.__INITIAL_STATE__
const clientDerivedState = buildInitialState({
  storage: readSafeLocalStorage(),
  cookie: readBrowserCookie(),
  now: Date.now()
})

console.info('hydration_debug', {
  requestId: window.__REQUEST_ID__,
  ssrPayload,
  clientDerivedState
})

Tujuan utamanya bukan logging permanen, melainkan membuktikan nilai mana yang pertama kali divergen.

3. Verifikasi invalidasi cache end-to-end

Jika snapshot SSR berbeda dari sumber data terbaru, periksa invalidasi cache dari hulu ke hilir:

  1. Apakah database atau service upstream sudah benar?
  2. Apakah cache aplikasi dibersihkan atau memiliki TTL yang tepat?
  3. Apakah HTML SSR, edge cache, atau data loader framework ikut diinvalidasi?
  4. Apakah cache key memasukkan semua pembeda yang relevan?

Kesalahan umum adalah invalidasi hanya satu lapis. Misalnya API fresh, tetapi HTML di CDN masih stale.

4. Bekukan faktor nondeterministik

Sebelum menyelidiki lebih jauh, nonaktifkan sementara faktor yang membuat hasil render berubah-ubah:

  • Ganti nilai waktu dengan timestamp tetap.
  • Matikan refetch otomatis saat mount.
  • Nonaktifkan eksperimen atau paksa satu feature flag.
  • Abaikan pembacaan localStorage untuk reproduksi.

Jika bug hilang setelah salah satu faktor dibekukan, Anda sudah mempersempit sumber drift.

5. Reproduksi dengan satu request yang dapat dikorelasikan

Bug hydration sering sulit ditelusuri karena log server dan browser terpisah. Gunakan requestId yang disisipkan ke HTML dan dibawa ke log klien. Dengan begitu Anda bisa mencocokkan:

  • respons HTML mentah,
  • payload awal yang disisipkan,
  • state klien saat hydration,
  • dan request API susulan setelah mount.

Pola verifikasi yang efektif

SSR snapshot audit

Buat daftar atribut state yang kritis terhadap render awal, lalu audit sumber nilainya di server. Contoh atribut kritis:

  • status login, role, subscription,
  • variant feature flag,
  • harga dan mata uang,
  • cart count, notification count,
  • locale, timezone, dan preferensi tampilan.

Untuk setiap atribut, dokumentasikan:

  • sumber data utama,
  • lapisan cache yang terlibat,
  • apakah nilainya boleh berbeda antara server dan klien,
  • dan kapan klien boleh melakukan koreksi setelah hydration.

Diff payload

Simpan dump minimal dari payload SSR dan state awal klien, lalu lakukan diff. Ini sangat membantu saat bug berasal dari satu field kecil seperti isEnabled, expiresAt, atau country.

// Pseudocode util sederhana
function diffKeys(a, b) {
  const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})])
  const diff = {}
  for (const key of keys) {
    if (JSON.stringify(a?.[key]) !== JSON.stringify(b?.[key])) {
      diff[key] = { ssr: a?.[key], client: b?.[key] }
    }
  }
  return diff
}

Di produksi, lakukan sampling dan pastikan data sensitif tidak tercatat.

Invalidasi cache

Jika masalah ternyata snapshot lama, validasi semua titik stale yang mungkin:

  • cache database/query,
  • cache aplikasi,
  • ISR/SSG revalidation atau mekanisme setara,
  • CDN/edge cache,
  • browser cache untuk dokumen atau data API.

Pastikan juga strategi invalidasi mengikuti model data. Data per-user jarang cocok untuk cache HTML global tanpa variasi key yang jelas.

Logging hydration

Tambahkan logging sementara pada fase awal klien:

  • payload yang diterima,
  • sumber override seperti storage/cookie,
  • timestamp hydration,
  • hasil evaluasi feature flag,
  • request API pertama setelah mount.

Logging ini membantu membedakan dua kasus:

  1. HTML server memang salah sejak awal.
  2. HTML server benar, tetapi klien menimpa state terlalu cepat dengan sumber lain.

Contoh konseptual di Next.js, Nuxt, dan SvelteKit

Detail API antar framework berbeda, tetapi pola masalahnya sama: ada data yang dipakai saat SSR, lalu ada sumber lain yang mengambil alih saat klien mulai aktif.

Next.js

Di Next.js, drift sering muncul ketika data halaman dirender di server dari satu sumber, lalu komponen klien membaca localStorage atau memicu fetch ulang segera setelah mount. Jika hasil fetch lebih baru daripada payload SSR, UI tampak “melompat”.

Pola aman:

  • Gunakan payload server sebagai satu-satunya state awal render.
  • Jika perlu sinkronisasi dari browser storage, lakukan setelah hydration dan beri transisi UI yang jelas.
  • Jangan gunakan nilai nondeterministik langsung di render pertama.
  • Untuk data yang sangat dinamis, pertimbangkan rendering placeholder stabil lalu update eksplisit di klien.

Nuxt

Di Nuxt, masalah biasanya terkait data yang diserialisasi dari server dan diambil ulang di klien, atau state store yang diinisialisasi dari sumber berbeda. Jika store menerima nilai dari cookie di server, tetapi plugin klien langsung menimpa dari storage lokal, mismatch mudah terjadi.

Pola aman:

  • Pusatkan inisialisasi state awal pada satu jalur.
  • Pastikan plugin klien tidak mengubah state sinkron pada awal hydration tanpa guard.
  • Pisahkan state yang wajib cocok saat SSR dari state personal yang boleh ditunda sampai klien aktif.

SvelteKit

Di SvelteKit, drift sering terjadi bila load di server menghasilkan data berbeda dengan perhitungan di browser setelah navigasi atau hydration, terutama untuk nilai berbasis waktu, cookie, dan environment browser-only.

Pola aman:

  • Jaga agar data dari load menjadi sumber render awal yang deterministik.
  • Gunakan guard untuk kode browser-only agar tidak memengaruhi HTML SSR.
  • Hindari pembacaan storage atau API browser dalam jalur render awal yang harus cocok dengan server.

Perbaikan yang biasanya efektif

1. Tetapkan satu sumber kebenaran untuk render awal

Jika halaman SSR, maka HTML dan hydration pertama harus berangkat dari snapshot yang sama. Jangan biarkan server memakai data A sementara klien membangun state awal dari data B.

2. Tunda override dari browser sampai setelah hydration

Untuk state personal seperti preferensi UI, baca dari localStorage setelah aplikasi ter-hydrate, bukan saat render awal yang harus cocok dengan SSR. Jika perubahan visual tak terhindarkan, tampilkan indikator loading atau lakukan update yang eksplisit.

3. Stabilkan evaluasi feature flag

Jika SSR bergantung pada feature flag, gunakan context evaluasi yang identik sebisa mungkin. Jika tidak bisa, hindari membuat struktur HTML awal bergantung pada flag yang bisa berubah saat hydration.

4. Hilangkan nilai nondeterministik dari render pertama

Jangan panggil Date.now() atau generator acak langsung di path render awal. Jika nilai waktu memang perlu ditampilkan, kirim timestamp dari server sebagai bagian dari payload dan gunakan nilai itu juga di klien.

5. Benahi cache key dan invalidasi

Pastikan cache tidak menggabungkan respons yang seharusnya berbeda. Jika status user, locale, atau varian eksperimen memengaruhi HTML, cache key harus mempertimbangkannya, atau HTML tersebut jangan di-cache secara global.

Common mistakes yang sering terlewat

  • Menganggap warning hydration hanya masalah kosmetik. Sering kali itu sinyal ada state flow yang salah.
  • Mengandalkan refetch cepat untuk “membetulkan” UI. Ini menutupi akar masalah dan menciptakan flicker.
  • Mencampur state SSR dan storage browser tanpa prioritas yang jelas.
  • Melupakan edge cache. Aplikasi terlihat benar di lokal, tetapi stale di produksi karena CDN.
  • Tidak membedakan state kritis render dan state opsional. Tidak semua state harus hadir saat SSR.

Checklist pencegahan hydration drift

  • Tentukan atribut state yang wajib identik antara SSR dan klien.
  • Pastikan HTML SSR dan payload embedded berasal dari snapshot yang sama.
  • Audit semua sumber state awal: backend, cache, embedded JSON, cookie, storage, feature flag, waktu render.
  • Gunakan satu jalur inisialisasi state awal; hindari override sinkron yang tersembunyi.
  • Tambahkan requestId untuk korelasi log server-klien.
  • Log snapshot SSR secara terbatas dan aman.
  • Lakukan diff payload saat investigasi bug produksi.
  • Periksa invalidasi cache pada semua lapisan, bukan hanya API.
  • Hindari nilai nondeterministik pada render pertama.
  • Untuk state personal yang tidak penting bagi SEO/first paint, pertimbangkan update setelah hydration.

Penutup

Debug hydration drift akibat state lama yang tersisip di SSR pada dasarnya adalah pekerjaan melacak sumber kebenaran state. Saat HTML server membawa snapshot lama atau tidak valid, klien akan “mengoreksi” UI dengan state yang berbeda, dan pengguna melihat perilaku yang terasa acak. Penyelesaiannya bukan sekadar membungkam warning, melainkan memastikan snapshot SSR, payload, cache, dan inisialisasi klien benar-benar selaras.

Jika Anda menemui bug seperti status yang sudah berubah tetapi kembali muncul sesaat lalu hilang lagi, curigai adanya snapshot lama yang masih hidup di salah satu jalur SSR. Mulailah dari audit HTML yang dikirim, diff payload, verifikasi invalidasi cache, lalu tambahkan logging hydration yang bisa dikorelasikan. Pendekatan ini jauh lebih efektif daripada menebak-nebak di level komponen.