Audit nilai default SSR diperlukan ketika tampilan hasil server-side rendering terlihat benar pada HTML awal, tetapi berubah beberapa milidetik kemudian setelah hydration. Masalah ini sering tidak memunculkan error keras; gejalanya justru berupa teks yang berganti, layout yang meloncat, tombol yang berubah status, atau urutan elemen yang tiba-tiba berbeda.
Akar persoalannya hampir selalu sama: server dan browser memulai render dari asumsi yang berbeda. Dalam diskusi yang lebih luas, tren sistem cerdas mendorong kebutuhan akan kejelasan asumsi dan nilai awal. Ide itu relevan juga di frontend: komponen SSR harus eksplisit tentang placeholder, locale, timezone, feature flag, media query, data user, dan state turunan. Jika nilai default di server tidak identik dengan nilai awal di client, hydration menjadi titik kejutan.
Rujukan konteks: pembahasan tentang kebutuhan memperjelas asumsi dalam sistem cerdas juga muncul dalam liputan The Economist mengenai mengapa laboratorium AI besar merekrut banyak filsuf. Intinya sama: sistem kompleks gagal secara halus ketika asumsi dasarnya tidak diaudit. Di frontend SSR, nilai awal adalah bentuk asumsi yang paling sering diabaikan.
Mengapa UI berubah saat hydration meski tidak ada error besar
Pada SSR, server mengirim HTML hasil render awal. Browser kemudian menjalankan JavaScript untuk menghubungkan event handler, state, dan efek ke markup tersebut. Proses ini disebut hydration. Idealnya, hasil render pertama di client sama persis dengan HTML dari server.
Masalah muncul ketika render awal di browser menghitung nilai yang berbeda dari yang dipakai server. Framework modern biasanya mencoba memulihkan kondisi ini. Kadang muncul warning mismatch, kadang tidak. Bahkan tanpa warning, pengguna tetap melihat perubahan visual seperti:
- teks placeholder berubah dari Loading... menjadi nama user secara mendadak,
- format tanggal berganti dari satu locale ke locale lain,
- tema terang berubah ke gelap setelah skrip client berjalan,
- komponen desktop berubah ke mobile karena media query baru dievaluasi di browser,
- fitur A/B test pindah varian setelah feature flag di-resolve di client,
- urutan list berubah karena sorting memakai timezone atau locale browser.
Secara teknis, ini bukan selalu “hydration gagal total”. Sering kali yang terjadi adalah initial render drift: markup awal valid, tetapi state awal client tidak sejalan dengan hasil render server.
Root cause: default yang implisit dan sumber kebenaran yang berbeda
Nilai default SSR bermasalah ketika salah satu kondisi berikut terjadi:
- Server tidak punya konteks yang dimiliki browser, misalnya ukuran viewport, preferensi tema, atau timezone lokal.
- Client menghitung nilai awal secara dinamis dari
window,localStorage, cookie, atau API browser sebelum state SSR disejajarkan. - Default placeholder di server berbeda dengan fallback data fetch di client.
- State turunan dihitung dua kali dengan input yang tidak identik, misalnya sorting berdasarkan locale default yang berbeda.
- Feature flag atau data user terlambat sehingga server merender varian A, tetapi client segera menggantinya ke varian B.
Prinsip auditnya sederhana: untuk setiap komponen SSR, tanyakan “nilai awal ini berasal dari mana, dan apakah server serta client melihat sumber yang sama?”
Checklist audit nilai default SSR
1. Audit placeholder dan fallback text
Placeholder sering dianggap aman, padahal justru paling sering menyebabkan perubahan visual. Contohnya, server merender Guest karena data user belum tersedia, lalu client langsung mengganti menjadi nama user setelah membaca session di browser.
- Pastikan fallback server dan nilai awal client identik.
- Jika data user tidak tersedia saat SSR, gunakan placeholder netral yang tidak mengubah layout drastis.
- Hindari mengganti struktur DOM hanya karena data belum siap; lebih aman mengganti isi teks di dalam struktur yang sama.
2. Audit locale dan timezone
Format tanggal, angka, mata uang, dan urutan sort bisa berubah jika server dan browser memakai locale atau timezone berbeda. Ini salah satu penyebab mismatch yang paling sulit dilacak karena tidak selalu meledak sebagai error.
- Jangan mengandalkan locale default runtime.
- Jika memungkinkan, kirim locale eksplisit dari request ke render server dan hydration payload.
- Untuk timezone yang tidak diketahui di server, pertimbangkan render format netral lebih dulu, lalu format ulang setelah mount jika memang perlu.
3. Audit feature flag
Feature flag yang dievaluasi di server dan client lewat sumber berbeda akan menyebabkan varian UI berubah setelah hydration.
- Gunakan hasil evaluasi flag yang sama untuk SSR dan initial client state.
- Jangan hitung ulang flag di client pada render pertama jika payload SSR sudah punya jawabannya.
- Catat versi atau snapshot flag agar mudah ditelusuri saat debugging.
4. Audit media query dan viewport-dependent UI
Server tidak mengetahui viewport nyata browser. Jika komponen memilih markup berbeda untuk mobile dan desktop pada render awal, Anda berisiko menghasilkan drift.
- Utamakan CSS responsif daripada branching markup di JavaScript.
- Jika markup memang harus berbeda, pertimbangkan client-only boundary untuk bagian tersebut.
- Jangan jadikan
window.matchMedia()sebagai penentu render SSR.
5. Audit data user dan sumber identitas
Status login, role, preferensi, dan izin akses harus berasal dari sumber yang konsisten.
- Jika server menggunakan cookie untuk identitas, client juga sebaiknya menerima hasil identitas itu dari payload SSR, bukan memulai dari
nulllalu memuat ulang. - Bedakan antara unknown, anonymous, dan authenticated. Banyak bug muncul karena semua kondisi ini disamakan menjadi default yang salah.
6. Audit state turunan
State turunan seperti hasil sort, group, filter, dan label tampilan sering dihitung ulang di client dengan input yang sedikit berbeda.
- Hindari menyimpan state turunan yang bisa dihitung dari data sumber, kecuali Anda benar-benar mengontrol inputnya.
- Pastikan fungsi derivasi deterministik: locale, timezone, pembanding sort, dan default filter harus eksplisit.
Contoh sebelum dan sesudah: pola bug yang umum
Kasus 1: locale dan timezone tidak eksplisit
Sebelum, server dan client sama-sama memformat tanggal dengan default lingkungan masing-masing:
// rentan berubah antar lingkungan
export function OrderDate({ createdAt }) {
const text = new Date(createdAt).toLocaleString();
return <time dateTime={createdAt}>{text}</time>;
}Masalahnya, default locale/timezone di server belum tentu sama dengan browser pengguna. Hasilnya, teks tanggal bisa berubah setelah hydration.
Sesudah, locale dibuat eksplisit, dan timezone diputuskan dengan sadar:
export function OrderDate({ createdAt, locale }) {
const text = new Intl.DateTimeFormat(locale, {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'UTC'
}).format(new Date(createdAt));
return <time dateTime={createdAt}>{text}</time>;
}Kenapa ini bekerja: server dan client memformat dengan input yang sama. Jika Anda memang ingin timezone lokal user, jangan pura-pura server mengetahuinya. Render dulu format stabil, lalu upgrade di client setelah mount jika kebutuhan UX mengharuskan.
Kasus 2: media query dipakai untuk menentukan markup awal
Sebelum, komponen bercabang berdasarkan API browser saat render:
function Nav() {
const isMobile = window.matchMedia('(max-width: 768px)').matches;
return isMobile ? <MobileNav /> : <DesktopNav />;
}Ini tidak aman untuk SSR karena server tidak punya window. Bahkan jika dibungkus pengecekan environment, nilai default server tetap spekulatif.
Sesudah, gunakan satu markup dasar dan serahkan responsivitas ke CSS:
function Nav() {
return (
<nav className="nav">
<button className="nav-toggle">Menu</button>
<ul className="nav-list">...</ul>
</nav>
);
}Jika perilaku benar-benar harus tergantung browser, jadikan hanya bagian interaktifnya yang membaca media query setelah mount, bukan seluruh struktur DOM awal.
Kasus 3: data user dimulai dari default yang salah
Sebelum, client memulai dari null walaupun server sebenarnya sudah tahu user:
function Header() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/me').then(r => r.json()).then(setUser);
}, []);
return <div>{user ? `Halo, ${user.name}` : 'Halo, Guest'}</div>;
}Akibatnya, HTML awal bisa menampilkan Guest, lalu berubah ke nama user segera setelah hydration.
Sesudah, gunakan data SSR sebagai initial state:
function Header({ initialUser }) {
const [user] = useState(initialUser);
return <div>{user ? `Halo, ${user.name}` : 'Halo, Guest'}</div>;
}Jika refresh data tetap diperlukan, lakukan setelah initial state tersinkron, dan usahakan perubahan berikutnya memang mencerminkan data baru, bukan koreksi atas default yang salah.
Pola perbaikan yang aman di Next.js, Nuxt, dan SvelteKit
Next.js
- Pastikan data yang sudah diketahui pada request dimasukkan ke render SSR dan dipakai lagi sebagai initial state di client.
- Hindari membaca
window,document,localStorage, ataumatchMediapada render awal komponen yang ikut SSR. - Untuk bagian yang benar-benar hanya masuk akal di browser, pertimbangkan boundary client-only atau pemuatan komponen khusus client. Gunakan ini secara selektif karena ada biaya pada waktu interaktif dan potensi layout shift.
- Jika memakai App Router atau pola server/client component, pisahkan tanggung jawab: server menentukan data awal yang stabil, client menambahkan interaktivitas di atas dasar yang sama.
Nuxt
- Gunakan state yang berasal dari payload SSR sebagai sumber kebenaran pertama.
- Berhati-hatilah terhadap kode yang menghasilkan default berbeda di server dan client, terutama pada composable yang membaca API browser.
- Untuk fitur yang bergantung pada browser, tunda evaluasi sampai komponen sudah berjalan di client, atau bungkus bagian tersebut sebagai client-only bila perbedaan markup tidak dapat dihindari.
SvelteKit
- Masukkan nilai awal melalui load atau data halaman sehingga server dan client merender dari input yang identik.
- Jangan menghitung nilai awal dari browser-only API pada saat markup SSR dibentuk.
- Gunakan logika yang berjalan setelah mount untuk enhancement yang memang bergantung pada lingkungan client.
Inti lintas-framework tetap sama: render awal harus deterministik dan berbasis input yang sama. Framework hanya memberi alat; masalah sebenarnya ada pada asumsi default yang tidak terdokumentasi.
Strategi logging untuk membuktikan sumber mismatch
Hydration drift sering terasa “acak” sampai Anda membandingkan input render server dan client secara sistematis. Logging yang baik harus fokus pada nilai awal, bukan sekadar error stack.
Log snapshot render awal
Catat nilai yang memengaruhi output HTML, misalnya:
- locale, timezone, dan format options,
- status user: unknown/anonymous/authenticated,
- snapshot feature flags,
- placeholder yang dipilih,
- hasil evaluasi media query jika ada,
- filter/sort default dan data inputnya.
Simpan log ini baik di server maupun client dengan korelasi request ID atau page ID yang sama. Tujuannya bukan logging permanen untuk semua pengguna, tetapi alat investigasi saat drift terdeteksi.
Bandingkan payload SSR dengan state hydration
Jika server sudah mengirim data awal, log apakah client benar-benar memakainya atau malah menghitung ulang dari sumber lain. Banyak mismatch terselesaikan hanya dengan menemukan bahwa komponen mengabaikan payload SSR dan memulai dari default internal.
Log perubahan visual yang tak memunculkan error
Tidak semua framework akan memberi sinyal keras. Karena itu, perhatikan indikator seperti:
- teks yang berubah dalam satu node setelah mount,
- class CSS yang berbeda sebelum dan sesudah hydration,
- jumlah elemen list yang berubah,
- atribut seperti
dir,lang, atau label ARIA yang berganti.
Untuk debugging lokal, Anda bisa menambahkan log sementara tepat sebelum render dan tepat setelah mount untuk membandingkan nilai:
// pseudocode lintas framework
log('render-input', {
locale,
timezone,
userState,
flags,
placeholder,
sortKey
});Yang penting bukan bentuk API logging-nya, melainkan disiplin untuk mencatat asumsi awal secara eksplisit.
Kapan memakai client-only boundary
Client-only boundary layak dipakai ketika hasil render awal memang tidak bisa dibuat deterministik di server tanpa biaya atau kompleksitas yang tidak sebanding. Contoh umum:
- komponen yang sepenuhnya bergantung pada API browser,
- widget pihak ketiga yang memodifikasi DOM sendiri,
- visualisasi yang sangat bergantung pada ukuran container setelah layout nyata terbentuk,
- UI personal yang tidak boleh ditebak server tanpa konteks user yang kuat.
Namun jangan memakai client-only sebagai pelarian default. Trade-off-nya nyata:
- HTML awal menjadi kurang informatif,
- SEO bisa menurun untuk konten penting,
- waktu sampai interaktif dapat memburuk,
- risiko layout shift tetap ada jika placeholder tidak dirancang baik.
Pilih client-only jika perbedaan lingkungan memang fundamental. Jika masalahnya hanya data awal tidak diteruskan dengan benar, perbaikannya seharusnya pada sinkronisasi state SSR, bukan mematikan SSR untuk komponen itu.
Pola keputusan praktis
- Jika nilai bisa diketahui di server, render di server dan kirim nilai yang sama ke client.
- Jika nilai tidak bisa diketahui di server tetapi tidak perlu mengubah struktur, render fallback stabil lalu upgrade setelah mount.
- Jika nilai tidak bisa diketahui di server dan menentukan struktur utama UI, pertimbangkan client-only boundary.
- Jika nilai merupakan state turunan, hitung secara deterministik dari input yang identik di kedua sisi.
Common mistakes yang sering terlewat
- Menggunakan default locale runtime tanpa sadar.
- Menganggap timezone server sama dengan timezone user.
- Membaca
localStorageuntuk tema pada render awal tanpa menyelaraskan HTML awal. - Menggunakan feature flag SDK yang mengevaluasi varian berbeda di client dan server.
- Membedakan markup besar berdasarkan media query JavaScript, bukan CSS.
- Menginisialisasi state dari props SSR, lalu segera menimpanya dengan efek fetch yang sebenarnya memuat data yang sama.
- Menghitung sort/filter dari data yang urutannya tidak stabil.
Penutup: audit asumsi awal, bukan hanya output akhir
Perubahan UI setelah hydration biasanya bukan masalah “framework SSR jelek”, melainkan masalah asumsi awal yang tidak selaras. Karena gejalanya sering halus dan tidak selalu memunculkan error keras, pendekatan terbaik adalah audit nilai default SSR secara sistematis.
Mulailah dari komponen yang paling sering berubah setelah page load. Periksa placeholder, locale, timezone, feature flag, media query, data user, dan state turunan. Jika server dan client tidak punya sumber kebenaran yang sama, Anda akan mendapatkan HTML awal yang tampak benar tetapi tidak stabil. Saat asumsi awal dibuat eksplisit, hydration menjadi proses penyambungan interaktivitas, bukan momen ketika UI “dikoreksi” di depan pengguna.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!