Hydration mismatch terjadi ketika Virtual DOM hasil kompilasi server-side rendering (SSR) oleh Nitro tidak identik dengan Virtual DOM yang dibuat browser saat proses inisialisasi client. Vue 3 terpaksa melakukan bailout atau patch paksa pada DOM nyata. Dampaknya terlihat jelas: muncul warning [Vue warn]: Hydration completed but contains mismatches pada browser console dan layout mengalami flicker (UI shift) yang merusak Cumulative Layout Shift (CLS).
Di Nuxt 3, dua penyebab utama insiden ini adalah format tanggal non-deterministik (timezone drift antara server dan client) serta kesalahan pemakaian state lokal ref() untuk menampung data asynchronous dari server.
Akar Masalah 1: Timezone Drift pada Tanggal
Server production (Docker container, serverless function, atau VPS) umumnya berjalan pada zona waktu UTC. Sementara itu, peramban klien mengeksekusi JavaScript menggunakan zona waktu lokal perangkat pengguna (misalnya, Asia/Jakarta atau GMT+7).
Ketika fungsi seperti new Date().toLocaleTimeString() atau Intl.DateTimeFormat tanpa opsi timezone eksplisit dipanggil di template, HTML server mengirim representasi waktu UTC, sedangkan browser merender ulang waktu lokal. Perbedaan teks ini langsung memicu error hydration mismatch.
Contoh Kasus Tanggal: Sebelum vs Sesudah
Hindari pemformatan berbasis local runtime langsung di root template.
Salah (Memicu mismatch):
<script setup lang="ts">
const date = new Date('2025-01-15T00:00:00Z')
</script>
<template>
<!-- Server (UTC) merender "15/01/2025", Client (GMT+7) merender "15/01/2025 07.00" -->
<div>{{ date.toLocaleString() }}</div>
</template>Benar (Format deterministik dengan timezone terkunci):
<script setup lang="ts">
const date = new Date('2025-01-15T00:00:00Z')
const formattedDate = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Asia/Jakarta' // Kunci timezone secara seragam
}).format(date)
</script>
<template>
<div>{{ formattedDate }}</div>
</template>Jika waktu harus mengikuti lokal pengguna secara dinamis, gunakan komponen bawaan <ClientOnly> dengan fallback visual yang simetris.
<template>
<ClientOnly>
<span>{{ new Date(post.createdAt).toLocaleTimeString() }}</span>
<template #fallback>
<!-- Ukuran fallback harus menyerupai teks akhir untuk mencegah CLS -->
<span class="skeleton-placeholder">--:--</span>
</template>
</ClientOnly>
</template>Akar Masalah 2: Kebocoran Boundary State (ref vs useState)
Komposisi state dengan ref() standar di level setup component hanya eksis dalam memori eksekusi lokal. Ketika server selesai merender HTML, data di dalam ref() tersebut di-garbage collect dan tidak disertakan ke dalam payload transfer SSR (window.__NUXT__).
Saat client hydration berlangsung, inisialisasi ref() berjalan ulang dari nilai awal (sering kali null atau array kosong), bukan dari hasil kalkulasi server. Ketidaksinkronan data ini merusak tree rendering.
Contoh State: Refaktor ke useState
Gunakan useState() agar state diserialisasi ke dalam SSR payload dan dihidrasi langsung ke client tanpa re-evaluation yang menghasilkan data berbeda.
Salah (State tidak masuk SSR payload):
<script setup lang="ts">
// State ini hilang saat transisi SSR ke client
const sessionToken = ref(Math.random().toString(36).slice(2))
</script>
<template>
<div>ID: {{ sessionToken }}</div>
</template>Benar (State ter-share via Nuxt payload):
<script setup lang="ts">
// useState menjamin nilai dieksekusi sekali di server dan diteruskan ke client
const sessionToken = useState('session_token', () => Math.random().toString(36).slice(2))
</script>
<template>
<div>ID: {{ sessionToken }}</div>
</template>Catatan Performa: Gunakan unique key yang spesifik pada
useState('key', init). Jika key saling tumpang tindih pada route berbeda, data dapat tertimpa tanpa sengaja.
Isolasi Eksekusi Client-Side via useHydrated
Untuk komponen interaktif yang sepenuhnya bergantung pada browser API (seperti window, localStorage, atau evaluasi media query CSS-in-JS), buat composable sederhana untuk memastikan rendering hanya terjadi setelah proses hidrasi selesai.
// composables/useHydrated.ts
import { ref, onMounted } from 'vue'
export function useHydrated() {
const isHydrated = ref(false)
onMounted(() => {
isHydrated.value = true
})
return isHydrated
}Gunakan di komponen tanpa memicu perbedaan tag saat tree DOM pertama kali dibangun:
<script setup lang="ts">
const isHydrated = useHydrated()
</script>
<template>
<div v-if="isHydrated">
<p>Screen Width: {{ window.innerWidth }}px</p>
</div>
<div v-else>
<p>Memuat preferensi layout...</p>
</div>
</template>Checklist Pencegahan Hydration Mismatch
- Kunci Timezone Server dan Format Date: Pastikan format string tanggal menetapkan timezone secara statis, atau serahkan pemformatan lokal seutuhnya ke client lewat
<ClientOnly>. - Gunakan useState untuk Data Dinamis: Jangan gunakan
ref()di luar fungsi lifecycle jika data tersebut dihasilkan saat SSR dan dibutuhkan kembali saat client mount. - Validasi Struktur Valid HTML: Hindari penempatan tag ilegal seperti tag blok
<div>atau<p>di dalam tag<p>, atau<tr>langsung di dalam<table>tanpa<tbody>. Browser otomatis memvalidasi DOM sebelum Vue melakukan hydration, yang menghasilkan DOM tree berbeda dari output SSR Nitro. - Hindari Evaluasi Nilai Acak di Render Template: Nilai seperti
Math.random(),crypto.randomUUID(), atauDate.now()wajib dimasukkan ke dalamuseState()jika dipanggil di server.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!