Hydration mismatch terjadi ketika representasi Virtual DOM (VNode) yang digenerate oleh Vue di client tidak cocok dengan struktur DOM aktual yang dihasilkan oleh server-side rendering (SSR). Di Nuxt 3, salah satu penyebab paling umum yang sering membingungkan developer adalah browser DOM reparenting akibat nesting tag HTML yang tidak valid.

Mekanisme Browser DOM Reparenting

Nuxt SSR merender komponen Vue menjadi string HTML mentah dan mengirimkannya ke browser. Browser modern membaca string tersebut menggunakan parser berbasis spesifikasi WHATWG HTML. Parser ini memiliki aturan perbaikan otomatis (spec-mandated error recovery) jika menemukan struktur tag yang melanggar spesifikasi HTML.

Ketika parser menemukan tag blok di dalam elemen inline pembatas (seperti elemen <div> di dalam <p>), parser secara implisit menutup tag pembuka sebelum memproses elemen blok berikutnya. Contoh lain adalah elemen interaktif bersarang seperti <a> di dalam <a> atau <button> di dalam <button>.

Perhatikan contoh template berikut:

<template>
  <p>
    Informasi akun:
    <div>Role: Administrator</div>
  </p>
</template>

String HTML yang dikirim oleh server Nuxt:

<p>Informasi akun:<div>Role: Administrator</div></p>

Browser membaca tag <div> saat tag <p> masih terbuka. Spesifikasi HTML melarang elemen blok berada di dalam <p>, sehingga browser menutup <p> secara otomatis sebelum merender <div>. Live DOM tree yang terbentuk di browser menjadi:

<p>Informasi akun:</p>
<div>Role: Administrator</div>
<p></p>

Setelah bundle JavaScript tiba, tahap hydration Vue dimulai. Vue Virtual DOM mengevaluasi bahwa root node adalah satu elemen <p> dengan dua child nodes. Namun, di DOM browser, elemen tersebut telah terpecah menjadi tiga node yang berbeda tingkatannya (reparented). Vue mendeteksi ketidaksesuaian ini dan mengeluarkan error:

[Vue warn]: Hydration node mismatch:
- Rendered on server: <p>
- Expected on client: <div>
[Vue warn]: Hydration completed but contains mismatches.

Diagnosis: Membandingkan Raw SSR Output dan Live DOM Tree

Debugging hydration mismatch tidak bisa dilakukan hanya dengan melihat tab Elements di Chrome DevTools, karena panel tersebut menampilkan DOM yang sudah dimutasi oleh browser parser dan Vue hydration.

Langkah 1: Ambil Raw HTML Langsung dari Server

Gunakan curl untuk membaca respons string asli tanpa intervensi browser:

curl -s http://localhost:3000/profil | grep -C 3 "Informasi akun"

Atau buka tab baru di browser dan gunakan skema: view-source:http://localhost:3000/profil. Halaman View Source menampilkan raw HTML dari server sebelum diproses oleh DOM parser.

Langkah 2: Bandingkan dengan DevTools Elements Tree

Buka DevTools pada halaman aktif. Periksa apakah elemen target berada pada hierarki yang sama persis dengan raw HTML. Jika di view-source elemen berada di dalam tag induk, tetapi di tab Elements elemen tersebut bergeser keluar menjadi sibling, kasus tersebut terkonfirmasi sebagai DOM reparenting.

Solusi 1: Refactoring Semantik HTML yang Valid

Langkah perbaikan paling tepat adalah memperbaiki markup agar sesuai standar spesifikasi HTML5.

Kasus: Elemen Blok di Dalam Paragraf

Ganti tag <p> dengan tag container generik seperti <div>, atau ubah child node menjadi elemen inline seperti <span>.

<!-- Sebelum: Invalid HTML -->
<template>
  <p>
    User Status
    <div class="badge">Active</div>
  </p>
</template>

<!-- Sesudah: Semantik Benar -->
<template>
  <div class="user-status">
    <p>User Status</p>
    <div class="badge">Active</div>
  </div>
</template>

Kasus: Struktur Tabel Tanpa Tbody

Spesifikasi browser akan menyisipkan <tbody> secara otomatis jika Anda menuliskan <tr> langsung di dalam <table>.

<!-- Sebelum: Browser menyisipkan <tbody> otomatis, memicu mismatch -->
<template>
  <table>
    <tr>
      <td>Data</td>
    </tr>
  </table>
</template>

<!-- Sesudah: Eksplisit deklarasi <tbody> -->
<template>
  <table>
    <tbody>
      <tr>
        <td>Data</td>
      </tr>
    </tbody>
  </table>
</template>

Solusi 2: Pencegahan via ESLint (eslint-plugin-vue)

Mencegah invalid HTML masuk ke tahap production memerlukan verifikasi statis pada pipeline linter. Konfigurasikan eslint-plugin-vue untuk menangkap invalid nesting saat build time.

Tambahkan aturan berikut pada file konfigurasi ESLint (.eslintrc.cjs atau flat config):

module.exports = {
  extends: [
    '@nuxtjs/eslint-config-typescript',
    'plugin:vue/vue3-recommended'
  ],
  rules: {
    // Memastikan parser template Vue mendeteksi parsing error standar
    'vue/no-parsing-error': ['error', {
      'invalid-first-character-of-tag-name': true,
      'nested-comment': true,
      'x-invalid-end-tag': true
    }],
    // Menolak struktur tag inline yang merender blok bawaan browser parser
    'vue/no-restricted-html-elements': [
      'error',
      {
        element: 'p',
        message: 'Gunakan <div> jika elemen ini akan menampung komponen anak bertipe blok.'
      }
    ]
  }
}

Solusi 3: Isolasi Third-Party Components Menggunakan ClientOnly

Terkadang nesting invalid berasal dari library pihak ketiga atau web component legacy yang menghasilkan struktur DOM tidak baku dan tidak dapat langsung Anda edit source code-nya. Solusi isolasinya adalah mengecualikan komponen tersebut dari tahap SSR menggunakan komponen bawaan Nuxt: <ClientOnly>.

<template>
  <div class="widget-wrapper">
    <!-- Komponen pihak ketiga yang memicu mutasi DOM dihindarkan dari SSR -->
    <ClientOnly fallback-tag="div" fallback="Memuat widget...">
      <ExternalLegacyWidget :data="data" />
    </ClientOnly>
  </div>
</template>

Catatan: Penggunaan <ClientOnly> harus diperlakukan sebagai fallback. Jika komponen tersebut memiliki dampak SEO atau layout shift (CLS), prioritaskan perbaikan struktur HTML pada level template atau ajukan pull request/patch ke library bersangkutan.