SSR privasi palsu terjadi saat aplikasi terlihat “aman” karena dirender di server, padahal state sensitif masih ikut muncul di HTML awal atau sempat tampil sebelum hydration memperbaiki UI di browser. Ini bukan sekadar masalah estetika atau warning hydration. Jika HTML awal sudah mengandung nama akun, status premium, hasil eksperimen, lokasi, atau fitur role tertentu, maka data itu sudah telanjur bocor ke browser, cache, log, extension, atau siapa pun yang bisa melihat source response.

Masalah utamanya sederhana: server harus menebak state client pada saat SSR. Jika tebakan itu salah, Anda mendapat dua risiko sekaligus: kebocoran data dan UI membingungkan. Pada hydration, framework akan menyamakan state client sebenarnya dengan HTML awal. Tetapi kalau konten sensitif sudah ter-render di respons pertama, perubahan setelah hydration tidak menghapus fakta bahwa data tersebut sempat dikirim.

Apa yang sebenarnya bocor saat SSR dan hydration?

Developer sering menganggap kebocoran hanya terjadi jika API mengirim data rahasia. Pada praktiknya, kebocoran juga terjadi ketika server merender UI berdasarkan asumsi yang tidak stabil, lalu browser mengoreksinya setelah hydration.

  • State sensitif di HTML awal: nama user, email, role admin, saldo, badge internal, eksperimen rahasia, eligibility promosi, atau lokasi.
  • Inference lewat UI: walau data mentah tidak tampil, tampilan komponen tertentu bisa mengungkap bahwa user termasuk segmen tertentu.
  • Mismtach yang menyesatkan: tombol “Admin”, “Premium”, “Diskon Khusus Kota X”, atau “Anda sudah check-in” muncul sesaat lalu hilang.

Jika konten sensitif sempat muncul di HTML response, masalahnya bukan lagi “warning hydration”, melainkan leak by initial render.

Sumber mismatch yang paling sering memicu SSR privasi palsu

1. localStorage dan state client-only

localStorage hanya ada di browser. Saat SSR, server tidak bisa membacanya. Jika UI bergantung pada flag seperti hasOnboarded, isInternalUser, selectedWorkspace, atau token non-HTTP-only yang disimpan di client, server akan merender fallback atau tebakan. Setelah hydration, state berubah.

Masalahnya muncul jika Anda merender sesuatu yang terlalu spesifik sebelum browser memverifikasi state sebenarnya.

2. Cookie yang tidak tersedia atau tidak konsisten

Cookie memang lebih ramah untuk SSR, tetapi tidak otomatis aman. Sumber mismatch antara lain:

  • Cookie hanya tersedia di client tertentu karena domain/path berbeda.
  • Cookie berubah setelah response awal.
  • Cookie diblokir, kedaluwarsa, atau tidak ikut pada request tertentu.
  • Anda mencampur cookie server dengan state client tambahan yang belum sinkron.

Jika server merender banner “Anda anggota Gold” hanya dari cookie yang belum tervalidasi, Anda tetap berisiko menampilkan informasi yang salah atau sensitif.

3. Geolocation

Lokasi berbasis browser biasanya memerlukan izin client. Server tidak punya informasi presisi yang sama. Merender promo, harga, atau fitur lokal sebelum client benar-benar mengonfirmasi lokasi bisa membocorkan aturan segmentasi internal atau menampilkan info yang salah.

4. Eksperimen A/B dan feature flag

Eksperimen sering dianggap aman karena “hanya UI”. Padahal varian eksperimen dapat mengungkap:

  • bahwa user masuk cohort tertentu,
  • bahwa fitur rahasia sedang diuji,
  • atau bahwa akun memiliki atribut tertentu.

Jika assignment eksperimen ditentukan di client tetapi SSR merender varian default atau tebakan, user bisa melihat varian yang berubah setelah hydration. Lebih buruk lagi jika HTML awal untuk varian internal terkirim ke semua orang akibat cache yang salah.

5. Waktu dan data yang berubah cepat

State yang bergantung pada waktu juga sering memicu mismatch: countdown, akses setelah jam tertentu, harga promo yang aktif dalam rentang sempit, atau status “online sekarang”. Selisih kecil antara waktu server dan browser dapat mengubah cabang render.

6. Fitur berbasis role atau permission

Ini yang paling berbahaya. Jika role ditentukan dari sumber yang berbeda antara server dan client, tombol atau panel sensitif bisa muncul sesaat. Walau endpoint backend tetap aman, UI itu sendiri sudah mengungkap keberadaan fitur internal, nama resource, atau alur admin.

Mengapa ini bukan sekadar masalah UX

Ada tiga alasan mengapa bug ini perlu diperlakukan sebagai isu keamanan dan privasi ringan hingga serius, tergantung konteks:

  1. HTML awal adalah data yang sudah dikirim. Setelah response sampai, konten bisa dibaca dari DevTools, proxy, extension, cache browser, rekaman session replay, atau log CDN tertentu.
  2. Hydration tidak membatalkan kebocoran. Menghapus elemen sesudah JavaScript jalan tidak sama dengan mencegah elemen itu dikirim.
  3. Inference tetap berbahaya. Kadang yang bocor bukan nilai mentah, melainkan fakta bahwa user termasuk kelompok tertentu.

Pola aman: jangan render data sensitif sebelum state final diketahui

1. Gunakan placeholder netral, bukan tebakan

Jika state hanya bisa diketahui di client, render placeholder yang tidak mengungkap apa pun. Hindari placeholder yang secara tidak langsung menyatakan hasil.

Kurang aman: menampilkan “Selamat datang, pengguna premium” lalu mengoreksinya setelah hydration.

Lebih aman: tampilkan skeleton, loading stub, atau area kosong dengan ukuran tetap sampai status final diketahui.

2. Pindahkan keputusan sensitif ke server bila memungkinkan

Jika UI bergantung pada autentikasi, role, entitlements, atau feature flag penting, idealnya server sudah punya sumber kebenaran yang stabil saat SSR. Artinya:

  • gunakan cookie sesi yang tervalidasi server,
  • ambil permission dari backend pada request yang sama,
  • hindari menyimpan fakta otorisasi penting hanya di localStorage.

Ini tidak selalu berarti semua harus SSR. Intinya, jangan merender konten sensitif dari asumsi yang hanya diketahui client.

3. Buat boundary jelas antara server dan client

Bedakan komponen yang aman dirender di server dari komponen yang memerlukan state browser. Bagian sensitif yang bergantung pada browser sebaiknya:

  • ditunda sampai mount selesai,
  • dibungkus komponen client-only,
  • atau dirender setelah verifikasi tambahan.

4. Stabilkan layout tanpa membocorkan isi

Developer kadang takut placeholder karena menyebabkan layout shift. Solusinya bukan merender data sensitif, tetapi membuat skeleton dengan tinggi/lebar yang konsisten. Jadi UI tetap stabil tanpa membocorkan isi.

Contoh bug dan perbaikannya di Next.js

Contoh tidak aman: state premium dari localStorage

Kasus umum: aplikasi menyimpan status UI atau entitlements di localStorage, lalu komponen langsung bercabang saat render.

// Client component
'use client'

import { useEffect, useState } from 'react'

export default function AccountBadge() {
  const [isPremium, setIsPremium] = useState(true) // tebakan berbahaya

  useEffect(() => {
    const value = window.localStorage.getItem('isPremium')
    setIsPremium(value === 'true')
  }, [])

  return isPremium
    ? <span className="badge">Premium Member</span>
    : null
}

Masalahnya: sebelum useEffect jalan, komponen sudah merender badge premium. Jika state sebenarnya false, badge mungkin hilang setelah hydration, tetapi HTML awal sudah telanjur menampilkan status premium.

Pola lebih aman: tri-state dan placeholder netral

'use client'

import { useEffect, useState } from 'react'

export default function AccountBadge() {
  const [isPremium, setIsPremium] = useState(null)

  useEffect(() => {
    const value = window.localStorage.getItem('isPremium')
    setIsPremium(value === 'true')
  }, [])

  if (isPremium === null) {
    return <span className="badge-skeleton" aria-hidden="true">&nbsp;</span>
  }

  return isPremium
    ? <span className="badge">Premium Member</span>
    : null
}

Mengapa ini lebih aman? Karena server dan render awal client sama-sama menghasilkan placeholder netral. Tidak ada status premium yang ditebak.

Bila data sensitif bisa diketahui di server, jangan ambil dari localStorage

Untuk role, subscription, atau entitlement, lebih baik server mengambil data dari sesi dan mengoper hasil yang sudah tervalidasi ke komponen. Dengan begitu HTML awal memang merepresentasikan state final yang sah.

// Pseudocode server-side idea
export default async function Page() {
  const session = await getSessionFromServer()
  const account = session ? await getAccount(session.userId) : null

  return <AccountHeader account={account} />
}

Trade-off-nya adalah tambahan kerja di server dan kemungkinan latensi lebih tinggi, tetapi ini jauh lebih baik untuk state sensitif daripada bergantung pada storage browser.

Menunda render komponen client-only

Untuk geolocation, eksperimen browser-only, atau state perangkat, gunakan komponen yang baru merender konten final setelah mount.

'use client'

import { useEffect, useState } from 'react'

export default function ClientOnly({ children, fallback = null }) {
  const [mounted, setMounted] = useState(false)

  useEffect(() => {
    setMounted(true)
  }, [])

  return mounted ? children : fallback
}

Gunakan wrapper ini hanya untuk area yang memang tidak aman atau tidak stabil bila di-SSR. Jangan membungkus seluruh halaman tanpa alasan, karena Anda akan kehilangan manfaat SSR.

Contoh di Nuxt.js

Contoh tidak aman: branch render berbasis browser API

<script setup>
const isNearby = ref(false)

if (process.client) {
  navigator.geolocation.getCurrentPosition(() => {
    isNearby.value = true
  })
}
</script>

<template>
  <div v-if="isNearby">Promo khusus area Anda</div>
</template>

Masalahnya bukan hanya geolocation yang asinkron, tetapi juga fakta bahwa SSR tidak punya state final. Jika Anda nanti menambahkan fallback yang terlalu spesifik, risiko kebocoran langsung muncul.

Pola lebih aman dengan client-only dan placeholder

<template>
  <ClientOnly>
    <template #fallback>
      <div class="promo-skeleton" aria-hidden="true">&nbsp;</div>
    </template>

    <LocationPromo />
  </ClientOnly>
</template>

Di dalam LocationPromo, baru lakukan akses geolocation dan render hasil setelah state final diketahui. Ini mencegah server menebak sesuatu yang tidak ia ketahui.

Data berbasis role di Nuxt

Untuk role dan permission, lebih aman mengambilnya dari request server atau endpoint internal yang tervalidasi sesi, lalu gunakan hasil itu sebagai sumber kebenaran saat SSR. Jangan menampilkan menu admin dari token yang hanya dibaca di browser.

Kapan harus menunda render, kapan harus SSR penuh?

Pilih SSR penuh jika:

  • state bisa diketahui secara andal di server,
  • konten sensitif perlu tampil di initial response secara benar,
  • SEO atau time-to-content penting.

Pilih tunda render/client-only jika:

  • state berasal dari browser API,
  • hasil bergantung pada izin user,
  • state tidak kritis untuk SEO,
  • atau salah render lebih berbahaya daripada sedikit penundaan UI.

Pilih placeholder netral jika:

  • Anda ingin layout stabil,
  • tetapi belum punya state final pada SSR,
  • dan konten final tidak boleh ditebak.

Kesalahan umum yang sering lolos code review

  • Default state terlalu optimistis, misalnya true untuk premium, admin, atau eligible.
  • Menganggap “nanti dikoreksi useEffect” itu aman. Aman untuk UX tertentu, tidak aman untuk privasi.
  • Menggunakan hidden CSS alih-alih tidak merender. Konten yang disembunyikan CSS tetap terkirim di HTML.
  • Mengandalkan client guard untuk menu atau panel sensitif padahal HTML awal sudah memuat label, link, atau metadata fitur.
  • Mencampur cache publik dengan konten personal. Bahkan SSR yang benar bisa bocor bila cache key tidak memisahkan sesi atau cookie yang relevan.
  • Eksperimen A/B diputuskan setelah render awal sehingga varian berganti saat hydration.

Checklist audit untuk mencegah kebocoran state saat hydration

Audit komponen

  1. Apakah komponen ini merender berbeda antara server dan client?
  2. Jika ya, apakah perbedaan itu melibatkan data sensitif, role, eligibility, lokasi, atau eksperimen?
  3. Apakah default state saat render awal bersifat netral?
  4. Apakah konten hanya disembunyikan dengan CSS, bukan benar-benar tidak dirender?

Audit sumber data

  1. Apakah state berasal dari localStorage, geolocation, waktu lokal, ukuran layar, atau API browser lain?
  2. Jika berasal dari cookie, apakah cookie itu benar-benar tersedia dan tervalidasi pada SSR?
  3. Apakah ada dua sumber kebenaran yang bisa berbeda antara server dan client?

Audit keamanan dan cache

  1. Apakah HTML awal bisa di-cache secara publik padahal berisi state personal?
  2. Apakah CDN/proxy memahami variasi berdasarkan cookie atau header yang relevan?
  3. Apakah session replay atau log frontend dapat merekam UI sensitif yang sempat tampil?

Audit pengujian

  1. Lihat View Source atau response HTML mentah, bukan hanya DOM setelah hydration.
  2. Nonaktifkan JavaScript untuk memeriksa apa yang benar-benar dikirim server.
  3. Throttle jaringan/CPU agar flash state lebih mudah terlihat.
  4. Uji dengan kombinasi role, cookie, geolocation denied, localStorage kosong, dan akun non-premium.
  5. Tambahkan snapshot test atau integration test untuk memastikan HTML awal tidak mengandung string sensitif.

Teknik debugging yang efektif

Periksa response mentah

Jangan hanya menginspeksi DOM setelah aplikasi stabil. Gunakan tab Network dan buka body response dokumen HTML. Di situ Anda bisa melihat apakah badge, menu, atau label sensitif sudah terkirim sebelum hydration.

Matikan JavaScript sementara

Jika halaman tanpa JavaScript masih menampilkan konten sensitif yang seharusnya client-only, berarti kebocoran terjadi di SSR, bukan sekadar saat interaksi client.

Cari string sensitif di build output atau HTML

Untuk kasus tertentu, Anda bisa mencari string seperti “Admin”, “Premium”, nama eksperimen, atau label internal pada HTML hasil render. Ini bukan pengujian sempurna, tetapi sering efektif menemukan kebocoran kasar.

Simulasikan state client yang bertentangan

Set cookie, kosongkan localStorage, tolak geolocation, lalu ulangi request. Tujuannya adalah memaksa kondisi ketika server dan client tidak sepakat, karena di situlah bug hydration paling mudah muncul.

Prinsip desain yang paling aman

Aturan praktisnya:

  • Jika server tidak tahu dengan pasti, jangan render hasil akhirnya.
  • Jika hasil akhirnya sensitif, jangan tebak.
  • Jika state hanya diketahui browser, render placeholder netral sampai browser selesai memverifikasi.
  • Jika state penting untuk security atau privacy, pindahkan sumber kebenaran ke server.

SSR tetap berguna untuk performa, SEO, dan perceived speed. Tetapi SSR bukan jaminan “private by default”. Begitu Anda menggabungkannya dengan hydration dan state client-only, asumsi privasi bisa runtuh secara halus: bukan lewat API yang terang-terangan bocor, melainkan lewat HTML awal yang terlalu banyak tahu atau terlalu cepat menebak.

Karena itu, anggap mismatch SSR-client bukan cuma isu rendering. Untuk state sensitif, ini adalah masalah desain boundary antara server dan browser. Boundary yang benar akan mencegah dua hal sekaligus: kebocoran data privat dan UI yang membuat user melihat sesuatu yang seharusnya tidak pernah tampil.