Hydration mismatch pada aplikasi SSR terjadi ketika HTML hasil render di server tidak identik dengan hasil render awal di browser. Masalahnya sering bukan pada komponen yang terlihat rumit, melainkan pada data tersembunyi: atribut yang diinjeksi pihak ketiga, metadata yang ikut terserialisasi, atau payload yang berubah diam-diam sebelum proses hydration selesai.
Ini penting diaudit karena gejalanya sering membingungkan: warning hanya muncul di console, event handler terasa tidak konsisten, state UI mendadak reset, atau framework memutuskan mengganti subtree DOM secara paksa. Konteks seperti isu perubahan kecil yang tidak terlihat namun memengaruhi perilaku aplikasi—seperti dibahas dalam topik prompt steganography—mengajarkan satu hal yang relevan untuk SSR: perbedaan kecil yang tersembunyi tetap bisa mengubah hasil eksekusi.
Kenapa Audit Hydration SSR Perlu Fokus ke Data yang Tidak Terlihat
Pada SSR, alurnya sederhana secara konsep:
- Server merender komponen menjadi HTML.
- HTML dikirim ke browser.
- Browser menjalankan JavaScript dan framework melakukan hydration dengan asumsi struktur dan data awal masih sama.
Hydration gagal atau memunculkan mismatch bila asumsi itu tidak terpenuhi. Penyebabnya tidak selalu berupa perbedaan teks yang kasat mata. Sering kali yang berubah adalah:
- Atribut tambahan seperti
data-*,style, atau penanda dari extension/browser script. - Payload state yang diurutkan berbeda, nilainya dibulatkan berbeda, atau sudah termutasi sebelum dipakai client.
- Metadata tersembunyi yang ikut diserialisasi dari server tetapi dibuang, difilter, atau ditambahkan ulang di client.
- Kode non-deterministik seperti
Date.now(),Math.random(), locale, timezone, atau pembacaan environment browser saat render awal.
Prinsip auditnya: bila server dan client membaca sumber kebenaran yang berbeda, sekecil apa pun selisihnya, hydration berisiko gagal.
Gejala Hydration Mismatch yang Paling Sering Muncul
1. Warning di console, tetapi UI terlihat “baik-baik saja”
Ini kondisi berbahaya karena tim sering mengabaikannya. Framework bisa melakukan recovery sebagian, namun recovery itu dapat menyebabkan subtree dirender ulang, event listener dipasang ulang, atau state lokal hilang.
2. Komponen interaktif terasa tidak stabil
Contohnya dropdown menutup sendiri, input kehilangan fokus, checkbox balik ke nilai awal, atau list mendadak reorder. Bukan karena logika event salah, tetapi karena node DOM yang di-hydrate tidak cocok lalu diganti.
3. Perbedaan hanya muncul di production
Di development, warning mungkin terlihat jelas. Di production, perilakunya bisa berbeda karena minifikasi, urutan script, cache, CDN transform, atau injeksi dari middleware/edge layer.
4. Mismatch hanya terjadi pada user tertentu
Biasanya terkait extension browser, personalisasi berdasarkan cookie, locale, timezone, A/B testing, atau response yang dimodifikasi oleh proxy.
Root Cause: Dari Metadata Tersembunyi sampai Payload yang Berubah Diam-diam
Atribut injeksi dari luar aplikasi
Extension browser, script analytics, widget pihak ketiga, atau alat observability kadang menambahkan atribut ke DOM sebelum hydration selesai. Misalnya penanda untuk pelacakan klik, highlight debug, atau style inline sementara. Jika atribut itu muncul di client tetapi tidak ada di HTML server, framework dapat mendeteksinya sebagai mismatch.
Serialisasi state yang tidak deterministik
Jika data awal dikirim dari server ke client melalui JSON, hasilnya harus konsisten. Sumber mismatch umum:
- Objek dibentuk dari beberapa source dengan urutan key yang berubah-ubah.
- Tipe data tidak stabil, misalnya
undefined,Date,Map,Set, atau class instance diperlakukan berbeda. - Nilai dibentuk dari operasi non-deterministik saat render.
- Filter sanitasi di client dan server tidak sama.
Data personalisasi yang berubah setelah HTML dibuat
Contohnya server merender berdasarkan cookie lama, tetapi client langsung mengambil data terbaru dari storage atau API sebelum hydration stabil. Akibatnya render pertama di browser tidak lagi sama dengan HTML yang sudah ada.
Kode yang seharusnya hanya jalan di browser, tetapi ikut memengaruhi render awal
Mengakses window, document, localStorage, ukuran viewport, media query, atau timezone browser saat menentukan output HTML adalah sumber mismatch klasik. Bahkan jika ada guard sederhana, hasil render bisa tetap berbeda bila fallback server dan nilai client tidak sinkron.
Transformasi HTML di jalur distribusi
CDN, middleware, sanitizer, atau plugin keamanan dapat mengubah HTML: menyisipkan nonce, memodifikasi atribut, menghapus tag tertentu, atau menulis ulang URL. Jika state serialisasi tidak ikut disesuaikan, hydration bisa gagal.
Checklist Investigasi Audit Hydration SSR
Sebelum mengubah banyak kode, lakukan audit sistematis berikut.
1. Bandingkan HTML server dengan DOM sebelum hydration selesai
Tujuannya memastikan apakah perubahannya sudah terjadi sebelum framework aktif penuh. Ambil snapshot:
- Response HTML mentah dari server.
- Isi
document.documentElement.outerHTMLsedini mungkin di browser test. - Payload state yang disisipkan ke halaman.
Jika DOM di browser sudah berbeda sebelum aplikasi aktif, kemungkinan ada injeksi eksternal atau transformasi response.
2. Audit sumber data awal
Buat daftar semua input yang memengaruhi render pertama:
- Props dari server
- Cookie dan header
- Environment variable publik
- Local storage atau session storage
- Waktu, locale, timezone
- Feature flag dan eksperimen
- Data hasil fetch ulang di client
Pertanyaan kuncinya: apakah server dan client membaca nilai yang sama pada render pertama?
3. Cari non-determinism
Telusuri penggunaan:
Date.now(),new Date()tanpa normalisasiMath.random()- sort tanpa comparator stabil
- generate ID saat render
- objek yang dimutasi in-place
4. Nonaktifkan extension dan script pihak ketiga
Uji di browser bersih, mode incognito, atau profil baru. Ini langkah sederhana tetapi sering langsung mengungkap sumber masalah, terutama bila mismatch berasal dari atribut injeksi.
5. Verifikasi apakah mismatch muncul di subtree tertentu
Jangan audit seluruh aplikasi sekaligus. Isolasi hingga Anda tahu komponen mana yang menghasilkan HTML berbeda.
Teknik Isolasi Komponen untuk Menemukan Sumber Mismatch
Gunakan pendekatan “potong subtree”
Nonaktifkan sementara komponen interaktif satu per satu, atau ganti dengan placeholder statis. Bila warning hilang, Anda sudah mempersempit area audit.
Bekukan props yang masuk
Log atau snapshot props hasil SSR dan props saat render awal client. Bila bentuknya sama tetapi hasil DOM berbeda, masalahnya ada pada logika render atau efek samping.
Beri penanda debug yang eksplisit
Tambahkan atribut debug sementara seperti data-debug-source="server" pada output kritis agar mudah dicocokkan saat inspeksi. Jangan biarkan ini permanen di production, tetapi sangat membantu saat audit.
Kurangi efek samping saat render
Pastikan render function atau template tidak mengubah state, tidak mendorong data ke array global, dan tidak membentuk nilai baru yang tidak stabil setiap kali dipanggil.
Stabilisasi Data Awal agar Hydration Konsisten
Serialisasi state secara deterministik
Gunakan struktur data yang bisa diserialisasi dengan stabil ke JSON. Hindari melempar object kompleks langsung ke state awal. Normalisasikan lebih dulu:
- Konversi
Datemenjadi string ISO. - Konversi
Map/Setmenjadi array atau object biasa. - Buang field sementara, function, dan nilai
undefinedbila tidak dibutuhkan. - Urutkan data bila output UI bergantung pada urutan.
Yang penting bukan formatnya “cantik”, tetapi server dan client memproses bentuk data yang sama.
Jangan gunakan data client-only untuk render pertama
Jika nilai berasal dari browser, render fallback yang sama di server dan client, lalu perbarui setelah mounted. Trade-off-nya adalah ada perubahan UI setelah hydration, tetapi itu lebih aman daripada mismatch pada render pertama.
Normalisasi locale dan timezone
Format tanggal atau angka yang bergantung pada locale sering menyebabkan hasil teks berbeda. Solusi praktis:
- Format final di server dan kirim sebagai string.
- Atau render placeholder netral lalu format di client setelah mounted.
- Gunakan timezone yang eksplisit bila output harus identik.
Sanitasi Atribut dan Markup Tersembunyi
Bila sumber mismatch adalah atribut tambahan atau metadata tersembunyi, pendekatannya bukan sekadar “abaikan warning”, melainkan mengendalikan jalur masuk data tersebut.
Batasi atribut yang ikut dirender
Jika komponen menerima object props besar lalu disebarkan ke elemen DOM, hati-hati terhadap pola semacam {...props} tanpa whitelist. Atribut tak terduga bisa lolos ke HTML server atau hanya muncul di client.
// React / Next.js - lebih aman dengan whitelist eksplisit
function Card({ title, id, className, testId }) {
return (
<section
id={id}
className={className}
data-testid={testId}
>
<h2>{title}</h2>
</section>
)
}
Whitelist membuat kontrak render lebih jelas dan mengurangi atribut liar dari object eksternal.
Sanitasi metadata sebelum serialisasi
Jangan kirim seluruh object backend mentah ke client bila hanya beberapa field yang dipakai UI. Selain lebih hemat, ini menghindari field internal yang bisa berubah atau diproses berbeda.
// Contoh normalisasi payload sebelum dipakai SSR
function normalizeArticle(raw) {
return {
id: String(raw.id),
title: raw.title ?? '',
author: raw.author?.name ?? 'Unknown',
publishedAt: raw.publishedAt ? new Date(raw.publishedAt).toISOString() : null,
tags: Array.isArray(raw.tags) ? [...raw.tags].sort() : []
}
}
Waspadai injeksi dari analytics dan widget
Bila script pihak ketiga menulis atribut ke node yang juga di-hydrate framework, pindahkan integrasinya:
- Tempelkan ke container yang tidak dikontrol SSR.
- Inisialisasi setelah hydration selesai.
- Gunakan elemen wrapper khusus agar script eksternal tidak memodifikasi subtree inti.
Guard untuk Kode yang Hanya Boleh Jalan di Client
Guard ini penting agar render server tidak bergantung pada API browser.
Contoh Next.js / React
import { useEffect, useState } from 'react'
export default function ThemeLabel() {
const [theme, setTheme] = useState('unknown')
useEffect(() => {
const value = window.localStorage.getItem('theme') || 'light'
setTheme(value)
}, [])
return <span>Tema: {theme}</span>
}
Pola ini aman bila server dan client sama-sama merender unknown pada render pertama. Setelah mounted, nilai baru diterapkan. Bila Anda langsung membaca localStorage saat render, hasil server dan client bisa berbeda.
Contoh Nuxt / Vue
<script setup>
import { ref, onMounted } from 'vue'
const theme = ref('unknown')
onMounted(() => {
theme.value = localStorage.getItem('theme') || 'light'
})
</script>
<template>
<span>Tema: {{ theme }}</span>
</template>
Prinsipnya sama: render awal harus stabil, lalu data browser-only diambil setelah mount.
Trade-off guard client-only
Guard mencegah mismatch, tetapi bisa menyebabkan content shift kecil setelah mounted. Untuk elemen kritis seperti tema atau preferensi layout, pertimbangkan menyelaraskan nilai lewat cookie yang dibaca server agar render pertama sudah akurat.
Contoh Audit Hydration SSR di Next.js
Kasus sederhana: server merender daftar item dari payload, tetapi client menambahkan metadata tersembunyi sebelum render sehingga urutan berubah.
// data yang datang dari server harus dinormalisasi dulu
function normalizeItems(items) {
return items
.map((item) => ({
id: String(item.id),
label: item.label ?? '',
priority: Number(item.priority ?? 0)
}))
.sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id))
}
export async function getServerSideProps() {
const raw = await fetchSomeData()
return {
props: {
initialItems: normalizeItems(raw)
}
}
}
export default function Page({ initialItems }) {
return (
<ul>
{initialItems.map((item) => (
<li key={item.id}>{item.label}</li>
))}
</ul>
)
}
Hal yang perlu diperhatikan:
- Urutan list harus deterministik.
keyharus stabil dan tidak dibentuk saat render.- Jangan menambahkan field dari browser sebelum render awal jika field itu memengaruhi urutan atau isi DOM.
Jika perlu enrich data di client, lakukan setelah mount dan pastikan perubahan itu memang perubahan UI yang disengaja, bukan perbaikan data awal yang terlambat.
Contoh Audit Hydration SSR di Nuxt
Pada Nuxt, sumber mismatch yang sering muncul adalah pemakaian state bersama yang terisi berbeda antara server dan client, atau transformasi data yang tidak konsisten.
<script setup>
const { data } = await useAsyncData('products', async () => {
const raw = await $fetch('/api/products')
return raw
.map((p) => ({
id: String(p.id),
name: p.name ?? '',
price: Number(p.price ?? 0)
}))
.sort((a, b) => a.id.localeCompare(b.id))
})
</script>
<template>
<ul>
<li v-for="product in data" :key="product.id">
{{ product.name }} - {{ product.price }}
</li>
</ul>
</template>
Di sini fokusnya bukan pada API Nuxt itu sendiri, melainkan memastikan transformasi data:
- menghasilkan tipe yang stabil,
- menggunakan urutan yang stabil,
- tidak bergantung pada environment browser saat render SSR.
Langkah Verifikasi di Development dan Production
Verifikasi di development
- Periksa console browser untuk warning hydration.
- Bandingkan HTML response dengan DOM awal.
- Uji dengan browser bersih tanpa extension.
- Matikan sementara analytics, widget chat, atau script eksperimen.
- Tambahkan log untuk payload SSR dan payload render awal client.
Verifikasi di production
Jangan anggap masalah selesai hanya karena local dev sudah bersih. Lakukan pemeriksaan ini:
- Ambil response HTML dari environment production yang nyata.
- Pastikan CDN, reverse proxy, atau edge middleware tidak mengubah markup secara diam-diam.
- Periksa apakah nonce CSP, atribut keamanan, atau penanda observability disisipkan ke subtree yang di-hydrate.
- Uji beberapa locale, timezone, dan status login.
- Uji di perangkat atau browser yang benar-benar dipakai pengguna.
Jika mismatch hanya muncul di production, prioritaskan audit jalur distribusi HTML dan script pihak ketiga. Banyak kasus bukan berasal dari komponen utama, tetapi dari modifikasi response di luar kode aplikasi.
Kesalahan Umum yang Sering Menyamar sebagai Bug Lain
- Menyalahkan state management padahal masalahnya ada pada payload awal yang tidak stabil.
- Menggunakan generated ID saat render untuk
keyatau atribut elemen. - Men-spread object mentah ke DOM dan membiarkan atribut tersembunyi ikut lolos.
- Membaca local storage saat render SSR-aware component.
- Mengabaikan warning karena UI tampak normal, padahal subtree sudah di-recover secara paksa.
Checklist Ringkas Pencegahan Hydration Mismatch
- Pastikan server dan client memakai data awal yang sama.
- Normalisasikan payload: tipe, urutan, dan field yang dipakai UI.
- Hindari API browser pada render pertama.
- Gunakan key yang stabil dan dapat diprediksi.
- Whitelist atribut DOM, jangan sembarang spread props.
- Isolasi script pihak ketiga dari subtree SSR utama.
- Audit transformasi HTML di CDN, proxy, dan middleware.
- Uji di browser bersih dan environment production nyata.
Penutup
Audit hydration SSR bukan hanya soal memperbaiki warning, tetapi memastikan kontrak antara HTML server dan render awal client tetap utuh. Dalam banyak kasus, mismatch muncul karena hal-hal yang tidak terlihat: metadata tersembunyi, atribut injeksi, payload yang berubah diam-diam, atau logika kecil yang tidak deterministik.
Pendekatan yang paling efektif adalah audit yang disiplin: bandingkan output server dan client, isolasi subtree bermasalah, normalisasikan state awal, sanitasi atribut, dan beri guard yang jelas untuk kode client-only. Begitu alur data awal Anda stabil dan deterministik, sebagian besar hydration mismatch akan hilang tanpa perlu solusi tambal-sulam.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!