Hydration mismatch terjadi ketika representasi Virtual DOM yang di-generate oleh klien pada tahap awal (initial client render) tidak identik dengan struktur HTML hasil kompilasi server (SSR). Pada arsitektur Inertia.js (menggunakan Vue 3 atau React), ketidakcocokan ini memicu DOM drift: node DOM terpaksa dirombak ulang secara paksa di browser, memicu hilangnya state internal, degradasi performa, hingga UI glitch.
Akar Masalah: Evaluasi Non-Deterministik Node.js vs Browser
Server Inertia SSR mengeksekusi kode komponen di runtime Node.js, sementara hidrasi berjalan di mesin JavaScript browser. Mismatch timbul saat komponen mengevaluasi state yang berbeda di kedua lingkungan tersebut:
- Perbedaan Format Tanggal dan Timezone: Server beroperasi di zona waktu UTC, sedangkan browser menggunakan zona waktu lokal pengguna (misal: GMT+7). Fungsi seperti
toLocaleDateString()akan menghasilkan string berbeda. - Akses Langsung ke Browser API: Membaca objek
window,document, ataulocalStoragelangsung saat render awal menghasilkanundefinedpada SSR, namun bernilai riil di browser. - Dynamic State Instan: Penggunaan fungsi non-deterministik seperti
Math.random()ataucrypto.randomUUID()saat inisialisasi state template.
Solusi 1: Normalisasi Tanggal Melalui ISO UTC
Hindari pemformatan tanggal langsung di level server template jika bergantung pada locale klien. Kirimkan raw timestamp (ISO-8601) dari Laravel/Inertia props, lalu tangani format spesifik klien hanya setelah komponen terpasang, atau gunakan tag semantik <time>.
Contoh perbaikan pada komponen Vue 3:
<!-- SEBELUM: Memicu Hydration Mismatch -->
<template>
<span>{{ new Date(post.created_at).toLocaleString() }}</span>
</template>
<!-- SESUDAH: Aman dari Mismatch -->
<script setup>
import { ref, onMounted } from 'vue';
const props = defineProps<{ post: { created_at: string } }>();
const formattedDate = ref(props.post.created_at); // Fallback deterministik (ISO)
onMounted(() => {
// Eksekusi mutasi hanya di runtime client
formattedDate.value = new Date(props.post.created_at).toLocaleString('id-ID', {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
});
});
</script>
<template>
<time :datetime='post.created_at'>{{ formattedDate }}</time>
</template>Solusi 2: Two-Pass Rendering Menggunakan Mounted Flag
Untuk komponen yang memerlukan akses ke localStorage, ukuran viewport (window.innerWidth), atau token sesi lokal, gunakan strategi two-pass rendering. Pass pertama merender representasi dasar yang cocok dengan output SSR, pass kedua memperbarui UI dengan state lokal.
<script setup>
import { ref, onMounted } from 'vue';
const isMounted = ref(false);
const userTheme = ref('light'); // Default deterministik untuk SSR
onMounted(() => {
isMounted.value = true;
userTheme.value = localStorage.getItem('theme') || 'light';
});
</script>
<template>
<div :class='userTheme'>
<!-- Jangan render blok khusus browser sebelum mount selesai -->
<span v-if='isMounted'>Tema aktif: {{ userTheme }}</span>
<span v-else>Memuat preferensi...</span>
</div>
</template>Solusi 3: Wrapper Komponen ClientOnly Tanpa Layout Shift
Abstraksikan logika penundaan render ke dalam satu komponen wrapper ClientOnly. Sediakan slot fallback dengan reservasi dimensi (height/min-height) untuk mengeliminasi Cumulative Layout Shift (CLS).
<!-- components/ClientOnly.vue -->
<script setup>
import { ref, onMounted } from 'vue';
defineProps<{
fallbackHeight?: string;
}>();
const isMounted = ref(false);
onMounted(() => {
isMounted.value = true;
});
</script>
<template>
<template v-if='isMounted'>
<slot />
</template>
<div
v-else
:style='{ height: fallbackHeight || "auto" }'
aria-hidden='true'
>
<slot name='fallback' />
</div>
</template>Penggunaan pada halaman Inertia:
<template>
<ClientOnly fallbackHeight='40px'>
<GeoLocationBanner />
<template #fallback>
<div class='skeleton-loader' style='height: 40px;' />
</template>
</ClientOnly>
</template>Verifikasi Hilangnya Warning di Browser Console
Jalankan proses SSR Inertia secara lokal untuk memverifikasi perbaikan:
npm run build
php artisan inertia:start-ssr
php artisan serveBuka DevTools pada browser (tab Console). Jika hydration mismatch masih terjadi, framework menampilkan log eksplisit:
- Vue 3:
[Vue warn]: Hydration node mismatchatauHydration text content mismatchdisertai path elemen terkait. - React:
Warning: Text content did not match. Server: "..." Client: "...".
Terapkan perbaikan di atas secara selektif. Gunakan normalisasi props ISO untuk data berbasis teks, dan manfaatkan ClientOnly hanya pada modul interaktif yang benar-benar membutuhkan API peramban.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!