Hydration mismatch di Nuxt.js sering terlihat sebagai warning di console, teks tanggal atau angka yang berubah setelah halaman tampil, dan UI flicker saat komponen di-hydrate di browser. Kasus yang paling sering adalah ketika server menghasilkan string tanggal/angka berdasarkan locale atau timezone tertentu, tetapi browser pengguna memformatnya dengan locale, timezone, atau waktu saat render yang berbeda.
Masalah ini bukan sekadar gangguan visual. Jika HTML hasil SSR tidak sama dengan output render awal di client, Vue akan memperingatkan adanya mismatch dan berusaha memperbaikinya saat hydration. Hasilnya bisa berupa perubahan teks mendadak, event binding yang terasa tidak konsisten, hingga sulitnya melacak bug yang hanya muncul di lingkungan tertentu.
Apa yang sebenarnya terjadi saat hydration
Pada SSR, Nuxt merender HTML di server lebih dulu. Ketika halaman diterima browser, Vue melakukan hydration: ia mencocokkan virtual DOM di client dengan HTML yang sudah ada. Proses ini mengasumsikan bahwa output render awal di client identik dengan output SSR.
Hydration mismatch terjadi ketika asumsi itu gagal. Dalam konteks locale dan waktu render, penyebab umumnya adalah:
- Locale berbeda antara server dan browser, misalnya server menghasilkan format
en-USsementara browser memakaiid-ID. - Timezone berbeda, misalnya server berjalan di UTC tetapi pengguna berada di Asia/Jakarta.
- Nilai non-deterministik saat render, seperti
new Date(),Date.now(), atauMath.random()yang dipanggil langsung di template atau setup. - Data tanggal ambigu, misalnya string waktu tanpa timezone yang di-parse berbeda oleh environment yang berbeda.
Prinsip dasarnya: semua nilai yang dipakai untuk merender HTML SSR harus menghasilkan output yang sama ketika komponen pertama kali dirender di client.
Gejala nyata yang sering muncul
1. Warning hydration di console
Anda bisa melihat pesan seperti node text content mismatch, children mismatch, atau peringatan bahwa hasil server tidak cocok dengan render client.
2. Teks tanggal atau angka berubah setelah halaman tampil
Contohnya, SSR menampilkan 10/12/2024, 08:00 AM, lalu setelah hydration berubah menjadi 10 Des 2024, 15.00.
3. UI flicker
Pengguna melihat teks yang sempat benar menurut SSR, lalu diganti dengan versi lain beberapa milidetik kemudian. Ini sangat umum pada komponen yang memformat tanggal relatif seperti “baru saja”, “5 menit lalu”, atau angka mata uang berbasis locale browser.
Root cause teknis: locale, timezone, dan waktu render
Locale berbeda menghasilkan string berbeda
API seperti Intl.DateTimeFormat dan Intl.NumberFormat bergantung pada locale. Jika SSR merender tanpa locale eksplisit, hasilnya bisa mengikuti default environment server. Di browser, hasilnya mengikuti preferensi pengguna atau runtime browser.
<script setup>
const amount = 1500000
const formatted = new Intl.NumberFormat().format(amount)
</script>
<template>
<p>{{ formatted }}</p>
</template>Kode di atas terlihat aman, tetapi new Intl.NumberFormat() tanpa locale eksplisit dapat menghasilkan string berbeda di server dan client.
Timezone berbeda menggeser tanggal/jam
Masalah lebih jelas pada tanggal dan waktu. Server mungkin merender dalam UTC, sementara browser pengguna merender dalam timezone lokal. Tanggal yang sama bisa tampil sebagai jam yang berbeda, bahkan bergeser ke hari sebelumnya atau berikutnya.
<script setup>
const publishedAt = '2024-12-10T23:30:00Z'
const formatted = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(publishedAt))
</script>Walaupun locale sudah ditentukan, timezone belum. Jika server dan browser memakai timezone default berbeda, output tetap bisa mismatch.
Nilai waktu saat render tidak stabil
Jika Anda memanggil Date.now() atau new Date() saat render, nilai itu hampir pasti berbeda antara SSR dan hydration. Selisih beberapa milidetik saja sudah cukup untuk mengubah output teks.
<script setup>
const label = new Date().toLocaleTimeString('id-ID')
</script>Output waktu dari server dan browser tidak mungkin konsisten karena keduanya dieksekusi pada momen berbeda.
Contoh komponen Nuxt.js yang bermasalah
Berikut contoh yang sering memicu hydration mismatch di Nuxt.js:
<script setup>
const props = defineProps({
publishedAt: {
type: String,
required: true
},
total: {
type: Number,
required: true
}
})
const publishedLabel = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'long',
timeStyle: 'short'
}).format(new Date(props.publishedAt))
const totalLabel = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR'
}).format(props.total)
const nowLabel = new Date().toLocaleTimeString('id-ID')
</script>
<template>
<div>
<p>Dipublikasikan: {{ publishedLabel }}</p>
<p>Total: {{ totalLabel }}</p>
<p>Dirender pada: {{ nowLabel }}</p>
</div>
</template>Masalah pada komponen ini:
publishedLabelmasih bergantung pada timezone default jika tidak ditentukan eksplisit.totalLabelrelatif aman bila locale konsisten, tetapi tetap berisiko jika locale sumbernya dinamis dan server/client tidak sinkron.nowLabelpasti non-deterministik karena dihitung saat SSR dan saat hydration pada waktu berbeda.
Perbaikan bertahap yang praktis
1. Tentukan locale dan timezone secara eksplisit
Jika output harus sama antara SSR dan client, jangan mengandalkan default environment. Tetapkan locale dan timezone yang sama di kedua sisi.
<script setup>
const props = defineProps({
publishedAt: String,
total: Number
})
const locale = 'id-ID'
const timeZone = 'UTC'
const publishedLabel = computed(() => {
return new Intl.DateTimeFormat(locale, {
dateStyle: 'long',
timeStyle: 'short',
timeZone
}).format(new Date(props.publishedAt))
})
const totalLabel = computed(() => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'IDR'
}).format(props.total)
})
</script>
<template>
<div>
<p>Dipublikasikan: {{ publishedLabel }}</p>
<p>Total: {{ totalLabel }}</p>
</div>
</template>Pendekatan ini cocok jika Anda memang ingin output SSR dan client identik, misalnya semua waktu ditampilkan dalam UTC atau timezone bisnis tertentu.
Trade-off: waktu yang tampil mungkin tidak sesuai timezone lokal pengguna.
2. Jika harus mengikuti timezone pengguna, render di client saja
Untuk tampilan yang memang harus sesuai browser pengguna, lebih aman memformat setelah komponen terpasang di client. Ini menghindari mismatch karena SSR tidak memaksakan string yang nanti berubah.
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps({
publishedAt: String
})
const publishedLabel = ref('')
onMounted(() => {
publishedLabel.value = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'long',
timeStyle: 'short'
}).format(new Date(props.publishedAt))
})
</script>
<template>
<p v-if="publishedLabel">Dipublikasikan: {{ publishedLabel }}</p>
<p v-else>Memuat waktu...</p>
</template>Alternatif lain adalah membungkus bagian tertentu dengan komponen client-only bila formatting benar-benar bergantung pada environment browser.
Trade-off: konten tidak sepenuhnya tersedia dari SSR, dan bisa ada placeholder sementara sebelum client selesai merender.
3. Hindari nilai non-deterministik saat render
Jangan memanggil Date.now(), new Date(), atau fungsi acak langsung untuk menghasilkan teks SSR yang harus sama di client.
Yang bermasalah:
<template>
<p>{{ new Date().toLocaleTimeString('id-ID') }}</p>
</template>Yang lebih aman:
- Kirim nilai waktu dari server sebagai data yang sudah tetap.
- Hitung di client setelah mounted jika nilainya memang harus “saat ini”.
- Gunakan timestamp dari API atau payload yang sama untuk SSR dan client.
4. Pakai computed yang aman dan berbasis input stabil
computed tidak otomatis menyelesaikan hydration mismatch, tetapi membantu bila perhitungannya hanya bergantung pada input yang sama di SSR dan client. Gunakan computed untuk memusatkan format yang deterministik, bukan untuk membungkus nilai yang berubah setiap waktu render.
<script setup>
const props = defineProps({
amount: Number,
locale: {
type: String,
default: 'id-ID'
}
})
const amountLabel = computed(() => {
return new Intl.NumberFormat(props.locale, {
style: 'currency',
currency: 'IDR'
}).format(props.amount)
})
</script>Selama props.locale dan props.amount identik antara SSR dan client, hasilnya cenderung stabil.
5. Normalisasi data waktu dari backend
Banyak mismatch muncul bukan karena Nuxt, tetapi karena format input tanggal ambigu. Hindari string tanggal yang tidak menyertakan timezone jika data lintas environment.
Lebih aman:
2024-12-10T23:30:00Zuntuk UTC2024-12-10T23:30:00+07:00untuk offset eksplisit
Lebih berisiko:
2024-12-10 23:30:002024/12/10
String ambigu bisa di-parse berbeda tergantung runtime dan konfigurasi environment.
Strategi debugging hydration mismatch di Nuxt.js
1. Bandingkan output SSR dan output setelah hydration
Mulailah dari elemen yang berubah. Buka halaman, lihat HTML awal, lalu bandingkan dengan hasil setelah browser selesai hydrate. Jika teks tanggal atau angka berubah sendiri, hampir pasti ada nilai yang tidak deterministik atau environment-dependent.
2. Tambahkan log yang jelas di server dan client
Log sederhana sangat membantu untuk melihat perbedaan locale, timezone, dan nilai render.
<script setup>
const props = defineProps({ publishedAt: String })
const locale = 'id-ID'
const serverNow = Date.now()
const parsed = new Date(props.publishedAt)
if (import.meta.server) {
console.log('[SSR]', {
publishedAt: props.publishedAt,
parsed: parsed.toISOString(),
serverNow,
formatted: new Intl.DateTimeFormat(locale, {
dateStyle: 'long',
timeStyle: 'short',
timeZone: 'UTC'
}).format(parsed)
})
}
if (import.meta.client) {
console.log('[CLIENT]', {
publishedAt: props.publishedAt,
parsed: parsed.toISOString(),
clientNow: Date.now(),
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
formatted: new Intl.DateTimeFormat(locale, {
dateStyle: 'long',
timeStyle: 'short'
}).format(parsed)
})
}
</script>Dari log ini, Anda bisa cepat melihat:
- apakah input tanggal sama,
- timezone browser apa yang dipakai,
- apakah hasil format server dan client berbeda.
3. Fokus pada node text mismatch
Jika warning mengarah ke node teks, periksa semua ekspresi template yang menghasilkan string dari tanggal, angka, locale, random, atau waktu saat ini. Ini jauh lebih sering jadi penyebab daripada struktur DOM kompleks.
4. Gunakan Vue Devtools untuk memeriksa state awal
Periksa nilai props, state, dan computed saat komponen pertama kali muncul di client. Jika state awal berbeda dari payload SSR, cari apakah ada transformasi yang berjalan hanya di browser.
5. Reproduksi dengan timezone berbeda
Bug ini sering tidak muncul di mesin developer yang timezone-nya kebetulan sama dengan server. Uji di environment berbeda atau set timezone yang berbeda saat menjalankan server pengembangan bila workflow tim memungkinkan. Tujuannya bukan mengandalkan trik environment tertentu, melainkan memastikan aplikasi tidak bergantung pada default yang tidak konsisten.
Pola solusi: pilih sesuai kebutuhan produk
Pilih SSR deterministik jika:
- konten tanggal/angka harus konsisten untuk semua pengguna,
- SEO penting,
- Anda ingin menghindari flicker semaksimal mungkin.
Strateginya: tetapkan locale dan timezone eksplisit, gunakan input waktu yang stabil, dan format saat SSR dengan aturan yang sama di client.
Pilih formatting di client jika:
- output harus mengikuti locale/timezone browser pengguna,
- perbedaan antar pengguna memang diharapkan,
- Anda menerima placeholder atau render tertunda untuk bagian tertentu.
Strateginya: render fallback netral di SSR, lalu format setelah mounted atau di area client-only.
Contoh refactor yang lebih aman
Berikut contoh refactor dari komponen bermasalah menjadi lebih aman untuk hydration:
<script setup>
import { computed, ref, onMounted } from 'vue'
const props = defineProps({
publishedAt: {
type: String,
required: true
},
total: {
type: Number,
required: true
},
locale: {
type: String,
default: 'id-ID'
}
})
// Stabil untuk SSR + client karena timezone ditentukan eksplisit
const publishedUtcLabel = computed(() => {
return new Intl.DateTimeFormat(props.locale, {
dateStyle: 'long',
timeStyle: 'short',
timeZone: 'UTC'
}).format(new Date(props.publishedAt))
})
const totalLabel = computed(() => {
return new Intl.NumberFormat(props.locale, {
style: 'currency',
currency: 'IDR'
}).format(props.total)
})
// Opsional: jika ingin tampilkan versi lokal pengguna, lakukan di client
const localPublishedLabel = ref('')
onMounted(() => {
localPublishedLabel.value = new Intl.DateTimeFormat(props.locale, {
dateStyle: 'long',
timeStyle: 'short'
}).format(new Date(props.publishedAt))
})
</script>
<template>
<section>
<p>Versi stabil SSR: {{ publishedUtcLabel }} UTC</p>
<p>Total: {{ totalLabel }}</p>
<p v-if="localPublishedLabel">Waktu lokal pengguna: {{ localPublishedLabel }}</p>
</section>
</template>Dengan pola ini, Anda memisahkan output yang harus stabil untuk hydration dari output yang memang bergantung pada browser.
Kesalahan umum yang sering terlewat
- Mengira locale saja cukup. Padahal timezone juga memengaruhi hasil format tanggal.
- Menggunakan
new Date()di template. Ini hampir selalu berisiko untuk SSR. - Mem-parse string tanggal ambigu dari API atau CMS.
- Menggunakan relative time saat SSR, misalnya “3 detik lalu”, yang langsung basi saat client hydrate.
- Mengandalkan environment default server untuk format angka dan tanggal.
Checklist pencegahan untuk tim frontend
- Jangan render nilai non-deterministik saat SSR: hindari
Date.now(),new Date(), dan random di template atau setup untuk output awal. - Tentukan locale eksplisit untuk formatting angka dan tanggal.
- Tentukan timezone eksplisit bila output SSR harus identik di semua environment.
- Jika harus mengikuti browser pengguna, format di client setelah mounted atau gunakan area client-only.
- Gunakan input tanggal yang jelas: utamakan ISO 8601 dengan timezone atau offset eksplisit.
- Pusatkan util formatting agar aturan locale/timezone tidak tersebar di banyak komponen.
- Log SSR dan client saat debugging untuk membandingkan input, output format, locale, dan timezone.
- Uji di timezone berbeda sebelum rilis, terutama untuk halaman yang menampilkan tanggal, jadwal, harga, dan angka lokal.
- Review warning hydration di development, jangan abaikan hanya karena tampilan terlihat “benar” setelah reload.
Penutup
Hydration mismatch di Nuxt.js karena locale dan waktu render biasanya berasal dari satu hal: output SSR tidak deterministik atau tidak konsisten dengan environment browser. Cara memperbaikinya bukan sekadar menekan warning, tetapi memastikan render awal memakai input, locale, dan timezone yang terkontrol.
Jika konten harus identik, buat formatting deterministik di SSR dan client. Jika konten memang harus mengikuti browser pengguna, pindahkan formatting ke client dan sediakan fallback yang masuk akal. Dengan pendekatan ini, Anda bisa menghilangkan warning hydration, mencegah teks tanggal/angka berubah sendiri, dan mengurangi flicker pada UI.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!