Hydration mismatch dari build-time config ke runtime UI biasanya terjadi ketika HTML hasil server tidak merepresentasikan state yang sama dengan yang dihitung browser saat JavaScript aktif. Dalam praktiknya, masalah ini sering dipicu oleh perbedaan environment variable, locale, feature flag, metadata deploy, atau sumber konfigurasi lain yang nilainya berubah antara tahap build, SSR, dan runtime client.
Ini makin sering terlihat pada workflow modern yang menekankan otomatisasi build dan deploy tanpa setup manual lokal, misalnya tren building and shipping apps without opening Xcode. Otomatisasi memang mempercepat rilis, tetapi juga memperbesar kemungkinan satu aplikasi dibangun dengan konfigurasi A, dirender di server dengan konfigurasi B, lalu di-hydrate di browser dengan konfigurasi C. Hasilnya bisa berupa warning hydration mismatch, UI berkedip sesaat, teks berubah setelah load, atau komponen yang salah branch rendering.
Kenapa mismatch ini terjadi?
Pada aplikasi web modern seperti Next.js, Nuxt, dan SvelteKit, ada setidaknya tiga fase yang relevan:
- Build-time: bundler mengganti nilai tertentu secara statis, mengoptimasi branch, dan menyematkan konfigurasi ke bundle.
- SSR/request-time: server merender HTML berdasarkan environment dan context request saat itu.
- Runtime client: browser menjalankan JavaScript, membaca state, cookie, locale, storage, atau endpoint konfigurasi lain, lalu mencoba meng-hydrate HTML yang sudah ada.
Hydration mismatch muncul jika output render pada fase ketiga tidak identik dengan markup yang dibuat di fase kedua. Perbedaan ini tidak harus besar. Satu teks, atribut, urutan node, class CSS, atau conditional rendering yang berbeda sudah cukup memicu warning atau re-render penuh.
Sumber perbedaan yang paling umum
- Environment variable publik vs privat: nilai yang tersedia saat build berbeda dari yang tersedia saat SSR atau di client.
- Feature flag: server memakai snapshot flag lama, client mengambil flag terbaru dari endpoint remote.
- Locale dan timezone: server menggunakan locale default container, browser memakai locale pengguna.
- Metadata deploy: versi app, region, commit SHA, atau branding berbeda antar artifact dan environment.
- Nilai default yang tidak stabil: fallback berbeda di server dan client.
- Akses API browser saat render: misalnya membaca
window,localStorage, ataunavigator.languageterlalu awal.
Gejala yang biasanya terlihat
Tidak semua hydration mismatch muncul sebagai error fatal. Banyak kasus hanya terlihat sebagai UI yang berubah sesaat setelah halaman dimuat.
- Warning seperti Text content does not match server-rendered HTML.
- Tombol, badge, banner, atau menu muncul lalu hilang.
- Label harga, mata uang, tanggal, atau bahasa berubah setelah hydration.
- Class CSS atau atribut
data-*berbeda antara server dan client. - Komponen tertentu di-render ulang penuh di sisi client.
- A/B variant berubah beberapa ratus milidetik setelah halaman tampil.
Jika pengguna melihat halaman “berkedip” atau state UI berubah tanpa interaksi apa pun, jangan langsung menyalahkan CSS atau cache. Sangat sering akar masalahnya adalah sumber konfigurasi yang tidak konsisten antar fase render.
Contoh root cause: env/config berbeda antara build, SSR, dan client
1. Feature flag dibekukan saat build, tetapi client mengambil nilai runtime
Misalnya Anda menyematkan flag publik ke bundle saat build, tetapi di browser Anda mengambil flag terbaru dari endpoint konfigurasi. Server merender versi lama, client menghitung versi baru.
// pseudo-code yang sering memicu mismatch
const showNewCheckout = PUBLIC_FLAG_NEW_CHECKOUT === 'true';
export default function CheckoutEntry() {
return showNewCheckout ? <NewCheckout /> : <OldCheckout />;
}
Lalu di client ada logic lain:
// setelah hydration atau saat bootstrap
const runtimeFlags = await fetch('/api/runtime-config').then(r => r.json());
const showNewCheckout = runtimeFlags.newCheckout;
Jika HTML server menampilkan OldCheckout tetapi browser langsung menilai newCheckout = true, mismatch hampir pasti terjadi.
2. Locale server dan browser berbeda
Kasus ini sangat umum pada formatting angka, tanggal, dan mata uang. Server container mungkin memakai locale default en-US, sedangkan browser pengguna memakai id-ID.
const price = 10000;
const label = new Intl.NumberFormat(undefined, {
style: 'currency',
currency: 'IDR'
}).format(price);
Memakai undefined untuk locale berarti hasilnya bergantung pada environment. Di server dan browser, outputnya bisa berbeda.
3. Runtime metadata deploy dipakai langsung untuk render awal
Build artifact bisa dibuat sekali lalu dipromosikan ke beberapa environment. Jika UI membaca metadata seperti region, channel, atau base URL dari sumber yang berubah per deploy, sementara sebagian nilai lain sudah baked-in saat build, komponen dapat memilih branch render berbeda.
4. Default value tidak sinkron
Contoh sederhana: server memakai fallback false untuk suatu flag jika env tidak ada, tetapi client memakai fallback true dari local storage atau bootstrap script. Fallback yang berbeda adalah penyebab mismatch yang sering terlewat.
Contoh aman: kirim snapshot config yang sama ke HTML dan client
Pola perbaikan yang paling aman adalah memastikan server dan client mengonsumsi snapshot konfigurasi yang identik untuk render awal. Jangan biarkan client menghitung ulang branch UI kritis dari sumber berbeda sebelum hydration selesai.
<script id="runtime-config" type="application/json">
{"locale":"id-ID","flags":{"newCheckout":false},"release":"2026-07-14"}
</script>
function readRuntimeConfig() {
const el = document.getElementById('runtime-config');
if (!el) return null;
return JSON.parse(el.textContent || '{}');
}
const config = readRuntimeConfig();
Dengan pola ini:
- SSR merender berdasarkan snapshot yang jelas.
- Client melakukan hydration dengan data yang sama.
- Pembaruan runtime bisa dilakukan setelah hydration, misalnya melalui refresh flag atau revalidation, tetapi perubahan itu menjadi update UI normal, bukan mismatch.
Pendekatan per framework
Next.js
Pada Next.js, masalah umum muncul ketika nilai tertentu diambil dari environment publik saat build, sementara SSR memakai env proses saat request, atau client mengambil config lagi dari endpoint terpisah. Prinsip utamanya:
- Jangan gunakan sumber config berbeda untuk branch render yang memengaruhi markup awal.
- Untuk data runtime, lebih aman ambil di server lalu teruskan sebagai props atau payload awal.
- Bedakan jelas mana config yang benar-benar statis dan mana yang harus request-aware.
- Hindari membaca
window,navigator, atau storage untuk menentukan tampilan awal saat komponen pertama kali render.
Nuxt
Pada Nuxt, pemisahan antara runtime config publik, data dari server, dan state client harus dijaga konsisten. Jika nilai config dipakai untuk menentukan struktur template, pastikan nilai yang dipakai pada render server sama dengan yang tersedia untuk hydration. Jika perlu, serialisasikan nilainya ke payload awal, bukan memuat ulang dari sumber lain sebelum mount selesai.
SvelteKit
Di SvelteKit, mismatch sering terjadi ketika load di server menghasilkan satu nilai, tetapi komponen atau kode browser menghitung nilai berbeda dari environment browser saat inisialisasi. Gunakan hasil load atau data yang di-serialize server sebagai sumber kebenaran render awal, lalu lakukan penyesuaian browser-only setelah mount bila memang diperlukan.
Strategi diagnosis yang bisa langsung diterapkan
1. Catat input render, bukan hanya error
Banyak tim hanya melihat warning hydration. Itu belum cukup. Anda perlu mengetahui nilai apa yang dipakai server dan client saat memutuskan branch render.
Minimal, log hal berikut pada server dan client:
- locale yang dipakai
- timezone bila relevan
- nama environment dan release ID
- feature flags yang memengaruhi markup
- source config: build-time, request-time, endpoint runtime, cookie, storage
// contoh pseudo-code logging terstruktur
const renderContext = {
release: process.env.RELEASE_ID,
locale,
flags,
configSource: 'ssr'
};
console.log('render-context', renderContext);
Di client, log konteks serupa setelah bootstrap. Jika ada perbedaan, Anda sudah punya kandidat root cause yang konkret.
2. Simpan hash snapshot config
Jika object config cukup besar, hitung hash ringan dari snapshot yang dipakai SSR dan bandingkan dengan yang dipakai client saat hydration. Ini memudahkan deteksi cepat tanpa mencetak seluruh object.
3. Reproduksi lokal dengan env yang sengaja berbeda
Jangan hanya menjalankan dev server. Banyak mismatch baru terlihat pada mode produksi karena bundling dan inlining env berbeda dari mode development.
- Build aplikasi dengan satu set env.
- Jalankan server hasil build dengan env lain.
- Akses halaman menggunakan browser dengan locale berbeda.
- Jika aplikasi memuat runtime config dari endpoint, ubah nilainya untuk mensimulasikan deploy aktual.
Tujuannya adalah meniru kondisi CI/CD: artifact dibangun sekali, lalu dijalankan di environment yang tidak identik dengan mesin build.
4. Lihat HTML mentah sebelum JavaScript aktif
Ambil HTML hasil SSR dari View Source atau response network. Bandingkan dengan DOM setelah hydration. Fokus pada node yang berbeda, terutama teks, atribut class, dan conditional subtree.
5. Matikan update runtime sementara
Jika Anda curiga endpoint runtime config menyebabkan flicker, paksa client memakai snapshot SSR saja selama debug. Jika mismatch hilang, sumber masalah hampir pasti ada pada refresh config terlalu dini.
Pola perbaikan yang aman
1. Tetapkan satu sumber kebenaran untuk render awal
Markup pertama harus ditentukan oleh satu snapshot data yang sama di server dan client. Jika flag atau locale memengaruhi struktur UI, serialisasikan nilainya ke HTML awal.
2. Tunda logic browser-only sampai setelah mount
Jika data memang hanya tersedia di browser, render placeholder yang stabil atau branch netral selama SSR dan hydration. Setelah komponen ter-mount, lakukan update normal.
// pseudo-code
let mounted = false;
onMount(() => {
mounted = true;
});
Trade-off-nya: Anda mungkin menambah satu fase update UI, tetapi setidaknya markup awal konsisten dan warning hydration hilang.
3. Jangan pakai locale implisit untuk output SSR
Tentukan locale secara eksplisit dari request, user preference, atau konfigurasi yang sudah disepakati. Hindari bergantung pada default OS/container.
4. Pisahkan config statis dan config runtime
Tidak semua konfigurasi pantas dibake ke bundle. Klasifikasikan:
- Statis: tidak berubah sepanjang artifact hidup.
- Runtime: bisa berubah per environment atau request.
- Browser-only: hanya relevan setelah aplikasi berjalan di sisi client.
Masalah muncul ketika satu nilai berada di area abu-abu, misalnya dianggap statis saat build tetapi diperlakukan runtime di browser.
5. Gunakan fallback yang identik
Jika env atau config tidak ada, fallback server dan client harus sama. Dokumentasikan default value dalam satu modul bersama agar tidak tersebar di banyak tempat.
6. Hindari branch render berdasarkan metadata deploy yang tidak stabil
Versi rilis, region, channel, atau info pipeline sebaiknya tidak menentukan struktur markup awal kecuali benar-benar dibutuhkan. Jika hanya untuk observability, tampilkan setelah mount atau gunakan untuk logging saja.
Contoh struktur config bersama
Daripada membaca env langsung dari banyak komponen, buat satu lapisan normalisasi config.
// config/shared.ts
export function normalizeConfig(input) {
return {
locale: input.locale || 'id-ID',
flags: {
newCheckout: Boolean(input.flags?.newCheckout)
},
release: input.release || 'unknown'
};
}
// server: bangun snapshot sekali
const initialConfig = normalizeConfig({
locale: requestLocale,
flags: serverFlags,
release: process.env.RELEASE_ID
});
// client: baca snapshot yang sama
const initialConfig = normalizeConfig(readRuntimeConfig());
Pola ini bekerja karena keputusan render awal bergantung pada representasi data yang sudah dinormalisasi dan konsisten.
Kesalahan umum yang sering terjadi
- Mengira semua env bisa diubah saat deploy, padahal sebagian sudah di-inline saat build.
- Mencampur feature flag dari bundle statis dengan runtime config tanpa prioritas yang jelas.
- Merender tanggal, angka, atau mata uang dengan locale default environment.
- Memakai cookie atau storage di client untuk branch render pertama, sementara SSR tidak punya nilai yang sama.
- Melakukan fetch config runtime terlalu cepat sehingga hasilnya menimpa state sebelum hydration stabil.
- Tidak menguji artifact produksi secara lokal dengan env yang berbeda dari env build.
Checklist pencegahan untuk CI/CD dan deploy
Saat build
- Dokumentasikan env mana yang benar-benar baked-in ke bundle.
- Hindari menyematkan flag yang sebenarnya perlu berubah per environment runtime.
- Simpan metadata build seperti release ID agar bisa ditelusuri di log.
- Bangun artifact produksi yang bisa diuji ulang secara lokal.
Saat SSR/deploy
- Pastikan locale default server/container tidak diam-diam berubah.
- Validasi runtime config yang disuplai ke server dan ke client berasal dari sumber yang sama.
- Pastikan fallback untuk env yang hilang terdokumentasi dan konsisten.
- Jika memakai remote flags, definisikan kapan snapshot diambil dan berapa lama dianggap valid.
Saat client runtime
- Gunakan payload awal dari server sebagai sumber render pertama.
- Tunda pembacaan browser-only state untuk hal yang tidak kritis terhadap markup awal.
- Jangan ubah subtree besar tepat saat hydration kecuali benar-benar perlu.
- Tambahkan logging untuk mendeteksi perbedaan config snapshot.
Saat observability dan debugging
- Kirim release ID, route, locale, dan hash config ke log atau error tracker.
- Simpan contoh HTML SSR untuk kasus yang gagal.
- Tambahkan test end-to-end untuk halaman yang sensitif terhadap locale dan feature flag.
- Uji skenario artifact built-once, deploy-many secara eksplisit.
Kapan mismatch tidak bisa dihindari sepenuhnya?
Ada kasus ketika UI memang harus menyesuaikan diri dengan informasi yang hanya tersedia di browser, misalnya preferensi dari storage lokal atau kemampuan perangkat. Dalam situasi seperti ini, targetnya bukan memaksa SSR mengetahui semuanya, tetapi mendesain render awal yang stabil. Gunakan placeholder, shell netral, atau enhancement setelah mount agar pengguna tidak melihat perubahan yang terasa acak.
Penutup
Hydration mismatch dari build-time config ke runtime UI hampir selalu berakar pada ketidakkonsistenan input render antar fase, bukan sekadar masalah framework. Pada pipeline modern yang sangat otomatis, terutama ketika build, SSR, dan deploy dipisahkan dengan jelas, perbedaan env, locale, flag, dan metadata makin mudah terjadi.
Solusi yang paling dapat diandalkan adalah menyamakan snapshot konfigurasi untuk render awal, menunda logic browser-only, dan membangun jalur diagnosis yang memperlihatkan input render secara eksplisit. Jika Anda memperlakukan config sebagai bagian dari kontrak render, bukan detail operasional semata, warning hydration dan UI flicker akan jauh lebih mudah dicegah.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!