Threat modeling untuk debug hydration mismatch di SSR adalah cara berpikir yang membantu Anda berhenti menebak-nebak penyebab mismatch antara HTML hasil server dan render awal di browser. Alih-alih langsung menyalahkan framework, kita petakan dulu aset yang harus konsisten, asumsi yang diam-diam dipakai kode, aktor yang memengaruhi output, lalu permukaan perubahan yang bisa membuat server dan client menghasilkan DOM berbeda.

Pendekatan ini sejalan dengan pola pikir praktis dari panduan threat model informal: mulai dari apa yang ingin dilindungi, siapa yang bisa memengaruhi, asumsi apa yang rapuh, lalu validasi apakah risikonya benar-benar nyata. Dalam konteks SSR, “ancaman” di sini bukan hanya penyerang, tetapi juga jam server, locale browser, state autentikasi, flag eksperimen, API yang hanya ada di browser, dan HTML invalid yang membuat parser browser memperbaiki markup secara diam-diam.

Apa sebenarnya yang rusak saat hydration mismatch?

Pada aplikasi SSR, server mengirim HTML awal agar halaman cepat tampil dan bisa diindeks. Setelah itu, JavaScript di browser melakukan hydration: framework memasang event handler dan menyelaraskan tree virtual dengan DOM yang sudah ada. Masalah muncul ketika hasil render awal di browser tidak identik dengan HTML dari server.

Gejalanya bisa berupa:

  • warning seperti Text content does not match server-rendered HTML,
  • node dipasang ulang oleh framework,
  • event handler tidak terikat pada elemen yang diharapkan,
  • UI berkedip karena subtree dibuang dan dirender ulang,
  • bug yang hanya muncul di produksi atau hanya pada locale tertentu.

Intinya, hydration mismatch hampir selalu berarti ada sumber nondeterminisme atau perbedaan lingkungan antara server dan client pada render pertama.

Memakai threat modeling untuk mendiagnosis mismatch

Alih-alih membuat daftar penyebab secara acak, pakai urutan berikut:

  1. Identifikasi aset: apa yang harus identik antara server dan client?
  2. Daftar asumsi: kondisi apa yang diam-diam dianggap sama?
  3. Identifikasi aktor: siapa atau apa yang dapat mengubah hasil render?
  4. Petakan permukaan perubahan: titik masuk nondeterminisme ada di mana?
  5. Validasi risiko nyata: buktikan dengan logging, snapshot, dan reproduksi minimal.

1. Aset: apa yang harus dijaga konsisten?

Dalam SSR, aset utamanya bukan hanya data bisnis, melainkan kesetaraan output render awal. Secara praktis, aset yang perlu Anda jaga adalah:

  • teks yang dirender server dan client,
  • struktur DOM dan urutan node,
  • atribut HTML yang memengaruhi diff,
  • state awal komponen,
  • hasil serialisasi data dari server ke client,
  • kondisi gating seperti auth, locale, timezone, dan feature flag.

Kalau satu saja berbeda, hydration bisa gagal total atau setidaknya menghasilkan patch ulang yang tidak Anda sadari.

2. Asumsi: di mana kode Anda terlalu percaya diri?

Sebagian besar mismatch lahir dari asumsi yang tidak diuji. Contoh asumsi berbahaya:

  • “Waktu di server dan browser akan menghasilkan string yang sama.”
  • “Locale default server sama dengan locale pengguna.”
  • “Nilai random hanya dipakai untuk dekorasi, jadi aman.”
  • “State login sudah sama saat render di server dan client.”
  • “Membaca window, localStorage, atau ukuran viewport saat render itu aman.”
  • “Feature flag pasti sudah ter-resolve sebelum HTML dirender.”
  • “Browser akan mem-parsing HTML persis seperti yang saya tulis.”

Threat modeling membantu Anda mengubah asumsi ini menjadi pertanyaan investigasi yang bisa dibuktikan.

3. Aktor: siapa yang memengaruhi hasil render?

Dalam debugging hydration mismatch, “aktor” tidak harus manusia. Aktor bisa berupa:

  • server runtime: timezone, locale default, environment variable, region deployment,
  • browser: locale pengguna, extension, parser HTML, API browser-only,
  • CDN atau edge: inject script, rewrite header, cache berbeda,
  • layanan auth: cookie, token refresh, user session,
  • layanan feature flag: evaluasi flag di server vs client,
  • kode aplikasi: helper tanggal, generator ID, sanitasi HTML, conditional rendering.

Tujuannya bukan memperluas masalah, tetapi memperjelas siapa yang boleh mengubah output render pertama.

4. Permukaan perubahan: dari mana mismatch biasanya masuk?

Untuk topik ini, ada tujuh permukaan utama yang paling sering memicu mismatch:

  • waktu dan timezone,
  • locale dan formatting,
  • random value atau ID non-deterministik,
  • state autentikasi,
  • data browser-only,
  • feature flag atau eksperimen,
  • HTML invalid.

Bagian berikut membahas masing-masing secara teknis.

Permukaan mismatch yang paling sering dan cara memeriksanya

Waktu, timezone, dan tanggal

Kasus klasik: server merender new Date() pada pukul tertentu, lalu client menghitung lagi beberapa milidetik kemudian. Jika output formatnya sensitif terhadap detik, menit, atau timezone, teks akan berbeda.

Masalah umum:

  • memanggil new Date() langsung di fungsi render,
  • memformat waktu relatif seperti “baru saja” atau “5 menit lalu” saat SSR,
  • server berjalan di UTC sementara browser pengguna memakai zona waktu lokal,
  • format default toLocaleString() bergantung pada environment.

Contoh rawan mismatch:

function PublishedAt({ iso }) {
  return <time>{new Date(iso).toLocaleString()}</time>
}

Kode ini tampak wajar, tetapi output server dan client bisa berbeda karena locale, timezone, atau format default.

Pendekatan lebih aman:

  • formatkan string final di server dan kirim sebagai string biasa, atau
  • pakai locale dan timezone eksplisit, atau
  • tunda format yang bergantung browser sampai setelah hydration.
function PublishedAt({ formatted, iso }) {
  return <time dateTime={iso}>{formatted}</time>
}

Jika memang harus memakai locale pengguna, render placeholder yang stabil saat SSR, lalu update setelah mount.

Locale dan formatting angka/mata uang

Hydration mismatch juga sering datang dari Intl atau fungsi formatting yang hasilnya berbeda karena locale default tidak sama. Misalnya server merender harga dengan pemisah ribuan tertentu, sementara browser memilih format lain.

Waspadai:

  • toLocaleString() tanpa locale eksplisit,
  • Intl.DateTimeFormat atau Intl.NumberFormat tanpa parameter yang terkunci,
  • fallback locale berbeda antara server dan client.

Praktik aman:

const formatter = new Intl.NumberFormat('id-ID', {
  style: 'currency',
  currency: 'IDR'
})

const priceText = formatter.format(price)

Jangan bergantung pada locale default jika output dipakai saat render awal.

Random value, ID dinamis, dan nilai non-deterministik

Setiap pemanggilan Math.random(), UUID acak, atau generator ID di render path berpotensi membuat node berbeda antara server dan client. Risiko ini besar jika nilai dipakai untuk:

  • key list,
  • id elemen,
  • atribut ARIA seperti aria-describedby,
  • teks yang tampil ke pengguna.

Contoh yang harus dihindari:

function Field() {
  const id = `input-${Math.random()}`
  return (
    <>
      <label htmlFor={id}>Email</label>
      <input id={id} />
    </>
  )
}

Alternatif:

  • gunakan ID stabil yang berasal dari data,
  • hasilkan ID di server lalu kirim sebagai prop,
  • gunakan mekanisme framework yang dirancang untuk ID stabil jika tersedia.

Untuk threat model, klasifikasikan semua sumber randomness sebagai permukaan perubahan prioritas tinggi karena efeknya sering menyebar ke struktur DOM.

State autentikasi dan personalisasi

Mismatch sering terjadi ketika server belum mengetahui state pengguna yang sama dengan browser. Misalnya:

  • server merender pengguna sebagai guest,
  • client menemukan token di storage atau hasil refresh session,
  • UI langsung berubah menjadi “Halo, Budi”.

Kalau perbedaan ini terjadi pada render pertama, hydration bisa mismatch. Ini umum pada navbar, menu akun, CTA login, dan konten personalisasi.

Debug pertanyaan:

  • Apakah server membaca cookie/session yang sama dengan client?
  • Apakah token hanya ada di localStorage?
  • Apakah ada refresh auth sebelum hydration selesai?
  • Apakah cache user state berbeda di server dan browser?

Pola lebih aman:

  • jadikan cookie/session yang dapat dibaca server sebagai sumber kebenaran untuk render awal,
  • hindari mengganti subtree auth-sensitive sebelum hydration selesai,
  • jika perlu, render state netral terlebih dahulu.

Data browser-only: window, localStorage, viewport, media query

SSR tidak memiliki window, ukuran layar aktual, status dark mode pengguna, atau isi localStorage. Jika render awal bergantung pada data ini, output client hampir pasti berbeda.

Contoh umum:

  • menampilkan ukuran viewport saat render,
  • membaca tema dari localStorage,
  • mengubah layout berdasarkan matchMedia,
  • mendeteksi browser capability sebelum mount.

Pola aman:

  • render fallback yang stabil di server,
  • baca API browser-only setelah mount,
  • pisahkan komponen yang benar-benar client-only bila framework mendukung.
function ThemeLabel() {
  const [theme, setTheme] = useState('system')

  useEffect(() => {
    const saved = window.localStorage.getItem('theme')
    if (saved) setTheme(saved)
  }, [])

  return <span>Tema: {theme}</span>
}

Di sini, output SSR harus tetap menggunakan nilai default yang sama dengan render awal client sebelum useEffect berjalan.

Feature flag dan eksperimen

Feature flag sering dianggap aman, padahal bisa memicu mismatch jika evaluasinya berbeda di server dan client. Contoh penyebabnya:

  • server memakai context pengguna yang belum lengkap,
  • client menghitung ulang flag dengan SDK berbeda,
  • bucket eksperimen memakai seed berbeda,
  • flag berubah karena network call yang selesai setelah HTML awal dikirim.

Pencegahan:

  • evaluasi flag untuk render awal di satu tempat, idealnya server,
  • serialisasikan hasil flag ke client,
  • jangan evaluasi ulang secara independen sebelum hydration selesai kecuali hasilnya dijamin sama.

Dalam threat model, feature flag adalah aktor tidak langsung yang dapat mengubah struktur UI. Perlakukan seperti dependency kritis, bukan sekadar toggle dekoratif.

HTML invalid dan parser browser

Ini sumber mismatch yang sering terlewat. Server mungkin menghasilkan string HTML tertentu, tetapi browser punya aturan parsing sendiri dan dapat “memperbaiki” markup invalid menjadi struktur DOM yang berbeda sebelum framework melakukan hydration.

Contoh masalah:

  • tag block di dalam tag yang tidak valid,
  • tag tidak tertutup dengan benar,
  • nested interactive elements yang tidak semestinya,
  • struktur tabel tidak valid.

Contoh rawan:

<p>
  Ringkasan:
  <div>Isi detail</div>
</p>

Browser bisa mengubah struktur DOM hasil parse sehingga berbeda dari yang dibayangkan render engine Anda.

Langkah debug:

  • lihat HTML mentah dari respons server,
  • bandingkan dengan DOM nyata di DevTools setelah parse tapi sebelum interaksi,
  • jalankan validator HTML atau linter template,
  • curigai komponen kaya markup seperti rich text, markdown, dan CMS content.

Checklist investigasi hydration mismatch

Gunakan checklist ini sebelum mengubah banyak kode:

  1. Reproduksi minimal
    Isolasi komponen paling kecil yang masih memunculkan mismatch.
  2. Tentukan jenis mismatch
    Apakah beda teks, atribut, urutan node, atau jumlah elemen?
  3. Bandingkan input render
    Catat props, state awal, cookie, locale, timezone, dan flag di server serta client.
  4. Cari nondeterminisme
    Grepping untuk Date, Math.random, crypto, toLocale, window, document, localStorage, matchMedia.
  5. Periksa cabang auth
    Apakah server dan client masuk branch UI yang sama?
  6. Periksa feature flag
    Apakah hasil evaluasi flag diserialisasikan dari server ke client?
  7. Validasi HTML
    Pastikan markup final valid dan tidak bergantung pada koreksi parser browser.
  8. Lihat data serialized
    Apakah payload hydration atau state awal berubah bentuk, hilang field, atau ter-truncate?
  9. Uji locale/timezone berbeda
    Coba user dengan locale dan zona waktu lain.
  10. Matikan extension dan script pihak ketiga
    Beberapa script dapat memodifikasi DOM sebelum hydration.

Logging yang relevan: apa yang perlu dicatat?

Debug hydration mismatch lebih cepat jika Anda mencatat input render, bukan hanya warning akhir. Prinsipnya: log kondisi yang membentuk HTML awal.

Log identitas render di server

const renderContext = {
  requestId,
  route: url.pathname,
  locale,
  timezone: process.env.TZ || 'system-default',
  authState: user ? 'authenticated' : 'guest',
  featureFlags,
  nowIso: new Date().toISOString()
}

console.log('[ssr-render]', JSON.stringify(renderContext))

Anda tidak harus memakai field yang sama persis, tetapi idenya adalah membuat snapshot kondisi SSR yang memengaruhi output.

Log state awal di client setelah mount

console.log('[client-hydration]', {
  route: window.location.pathname,
  locale: navigator.language,
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
  authState: window.__USER__ ? 'authenticated' : 'guest',
  featureFlags: window.__FLAGS__
})

Perbandingan dua log ini sering langsung memperlihatkan akar masalah, misalnya server merender locale en-US sedangkan browser memakai id-ID.

Log output yang dicurigai, bukan seluruh aplikasi

Jika satu komponen dicurigai, log input dan hasil render yang terpotong di sekitar komponen itu. Hindari logging besar-besaran yang sulit dibaca. Fokus pada:

  • string tanggal yang sudah diformat,
  • ID elemen yang dihasilkan,
  • hasil evaluasi auth/flag,
  • markup dari rich text atau CMS.

Catatan: Jangan log data sensitif seperti token, cookie mentah, atau payload user lengkap. Untuk auth, cukup log status dan metadata minimal yang relevan.

Contoh alur debug praktis di Next.js, Nuxt, dan SvelteKit

Framework berbeda, tetapi pola investigasinya sama: temukan komponen, bandingkan input SSR dan client, lalu hilangkan nondeterminisme dari render pertama.

Next.js

  1. Baca warning hydration di console
    Catat komponen atau subtree yang disebutkan.
  2. Isolasi komponen
    Pindahkan logic mencurigakan seperti formatting tanggal atau pembacaan window ke komponen kecil.
  3. Bandingkan data server vs client
    Jika data berasal dari server component, loader, atau API route, log output akhirnya sebelum dirender.
  4. Curigai client component
    Terutama yang memakai effect, browser API, feature flag SDK, atau state auth dari storage.
  5. Periksa HTML invalid
    Jika warning tidak jelas, lihat HTML dari respons dan struktur DOM di DevTools.

Tip: Jika mismatch hilang setelah logic tertentu dipindahkan ke effect atau komponen client-only, berarti Anda sudah menemukan permukaan perubahan. Setelah itu, putuskan apakah yang benar adalah menunda render, menstabilkan input, atau memindahkan sumber kebenaran ke server.

Nuxt

  1. Periksa data dari server rendering dan payload client
    Pastikan nilai yang diharapkan benar-benar sama pada navigasi awal.
  2. Audit composable
    Composables yang membaca waktu, locale browser, storage, atau auth state sering menjadi sumber mismatch.
  3. Periksa plugin yang hanya masuk di client
    Plugin ini bisa mengubah state global sebelum komponen stabil.
  4. Validasi branch rendering
    Pastikan guest vs authenticated atau flag on vs off tidak berubah di sela-sela hydration.

Tip: Dalam Nuxt, mismatch sering terasa seperti masalah data fetching, padahal sumbernya ada pada formatting atau state yang berasal dari plugin client-side.

SvelteKit

  1. Mulai dari load dan props
    Pastikan data yang dikirim ke halaman sama dengan yang dipakai saat client mengambil alih.
  2. Audit code path reaktif
    Reaktivitas yang membaca browser API terlalu dini bisa menghasilkan output berbeda.
  3. Periksa penggunaan variabel yang dihitung saat module init atau render
    Nilai seperti waktu sekarang atau locale browser dapat berbeda tanpa terlihat jelas.
  4. Bandingkan markup final
    Jika konten berasal dari markdown atau CMS, validasi HTML hasilnya.

Tip: Pada SvelteKit, sumber mismatch sering tersembunyi dalam ekspresi template yang tampak sederhana, misalnya memanggil helper format yang ternyata bergantung pada environment.

Contoh threat model ringkas untuk satu kasus nyata

Misalkan navbar SSR kadang mismatch. Di server tertulis Masuk, di browser berubah menjadi avatar pengguna.

Aset

  • HTML awal navbar, termasuk branch guest atau authenticated.

Asumsi

  • Server dan client mengetahui status login yang sama saat render awal.

Aktor

  • browser dengan localStorage,
  • cookie/session di server,
  • SDK auth client yang me-refresh token.

Permukaan perubahan

  • state auth dibaca dari storage di client,
  • server hanya melihat cookie yang belum sinkron.

Validasi risiko nyata

  • log status auth di server saat SSR,
  • log status auth di client sebelum dan sesudah mount,
  • cek apakah branch UI berubah sebelum hydration selesai.

Perbaikan

  • pindahkan sumber kebenaran render awal ke cookie/session yang tersedia di server,
  • atau render navbar netral sampai status auth benar-benar diketahui secara konsisten.

Dengan format ini, Anda tidak hanya memperbaiki bug saat ini, tetapi juga mendokumentasikan pola masalah untuk komponen lain.

Langkah pencegahan agar mismatch tidak berulang

Buat render awal deterministik

  • jangan panggil waktu sekarang langsung di render path,
  • jangan gunakan random value untuk HTML awal,
  • jangan bergantung pada locale default environment.

Bedakan data SSR-safe dan browser-only

  • data SSR-safe boleh dipakai di render awal,
  • data browser-only harus dibaca setelah mount atau lewat boundary client-only.

Serialisasikan keputusan penting dari server ke client

  • hasil feature flag,
  • status auth untuk render awal,
  • locale yang dipakai,
  • string hasil formatting jika perlu.

Validasi HTML di pipeline

  • aktifkan linter template atau JSX,
  • uji komponen rich text, markdown, dan konten CMS,
  • curigai wrapper yang menghasilkan nesting invalid.

Tambahkan guardrail di code review

Buat checklist review singkat untuk komponen SSR:

  • Apakah ada penggunaan Date, Math.random, atau toLocale* di render?
  • Apakah ada akses browser API sebelum mount?
  • Apakah auth dan feature flag konsisten antara server dan client?
  • Apakah markup final valid?

Uji pada environment yang berbeda

  • jalankan pengujian dengan timezone berbeda,
  • uji locale berbeda,
  • uji kondisi guest dan authenticated,
  • uji dengan flag aktif dan nonaktif.

Hydration mismatch sering lolos di mesin developer karena environment terlalu seragam. Variasi kecil pada locale atau auth state cukup untuk memunculkan bug di produksi.

Kesalahan umum saat memperbaiki hydration mismatch

  • Hanya menyembunyikan warning
    Ini tidak menghilangkan akar perbedaan render.
  • Memindahkan semua hal ke client-only tanpa alasan
    Kadang berhasil, tetapi Anda kehilangan manfaat SSR dan bisa memperburuk performa atau SEO.
  • Menganggap mismatch hanya masalah teks
    Padahal ID, atribut, dan struktur DOM juga kritis.
  • Tidak membandingkan input render
    Warning hydration sering terlambat; input-lah yang perlu diverifikasi.
  • Tidak memvalidasi HTML
    Parser browser bisa mengubah struktur sebelum framework bekerja.

Penutup

Threat modeling untuk debug hydration mismatch di SSR berguna karena memaksa kita fokus pada hal yang benar: aset yang harus konsisten, asumsi yang rapuh, aktor yang memengaruhi output, permukaan perubahan, dan bukti risiko nyata. Dengan cara ini, bug yang semula terasa acak bisa dipersempit menjadi pertanyaan teknis yang dapat diuji.

Jika Anda menghadapi mismatch di Next.js, Nuxt, atau SvelteKit, mulai dari tujuh permukaan utama: waktu, locale, random value, auth state, data browser-only, feature flag, dan HTML invalid. Dokumentasikan input render, bandingkan kondisi server dan client, lalu perbaiki dengan membuat render awal deterministik. Itulah cara paling konsisten untuk mengatasi hydration mismatch tanpa menebak-nebak.