Akar Masalah Hydration Mismatch pada SSG Zig

Static Site Generator (SSG) modern berbasis Zig seperti Zine menghasilkan dokumen HTML mentah secara deterministik dan berkinerja tinggi. Namun, saat menyematkan elemen UI interaktif seperti Web Components, Preact, atau Svelte islands ke dalam output statis tersebut, hydration error sering terjadi. Masalah ini berakar dari perbedaan antara struktur DOM statis yang di-serialize compiler Zig dan DOM tree in-memory yang dibangun oleh browser parser sebelum script client-side dieksekusi.

Kompiler Zig memproses string template secara literal tanpa pohon DOM semantik bawaan browser. Akibatnya, ada tiga titik kegagalan utama:

  • Whitespace Collapse dan Text Nodes: Compiler Zig mempertahankan newline dan indentation template secara presisi. Sebaliknya, parser browser memadatkan whitespace tertentu ke dalam TextNode terpisah atau menggabungkannya. Runtime hidrasi JavaScript menganggap perbedaan jumlah TextNode sebagai struktur yang rusak.
  • Void Tags dan Auto-closing: Elemen seperti <input>, <img>, atau <br> tidak memerlukan tag penutup. Jika template Zig menutupnya dengan sintaks XML self-closing (misalnya <input />) di luar konteks SVG, parser HTML5 browser akan mengabaikan tanda slash tersebut dan mengatur ulang hierarki elemen di bawahnya.
  • Atribut Custom Element: Atribut boolean pada HTML statis sering kali di-serialize menjadi disabled="" atau atribut tanpa nilai. Jika library hidrasi client-side mengevaluasi atribut tersebut dengan tipe data non-string (seperti ekspresi boolean eksplisit), proses rekonsiliasi VDOM akan gagal.

Kasus Nyata: Kerusakan Layout Akibat Mismatch

Pertimbangkan skenario widget counter interaktif berbasis Web Component yang disematkan ke dalam layout Zine:

<!-- Template Zig / Zine -->
<interactive-counter data-count="0">
  <button class="btn-dec">-</button>
  <span class="display">0</span>
  <button class="btn-inc">+</button>
</interactive-counter>

Jika generator Zig menyisipkan format whitespace antar-tag seperti:

<interactive-counter data-count="0">
  <button class="btn-dec">-</button>
  <!-- baris kosong tersembunyi atau spasi template engine -->
  <span class="display">0</span>
</interactive-counter>

Browser merender tree ini dengan TextNode kosong di antara elemen button dan span. Ketika script hidrasi mengeksekusi rekonsiliasi VDOM berbasis indeks anak (child node index), node target bergeser. Script mencoba menempelkan event listener penambahan nilai ke TextNode kosong, bukan ke tag <span>. Akibatnya, layout berkedip (FOUC), event listener tidak terpasang, dan browser menampilkan error di konsol:

Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.

Langkah Debugging Menggunakan DevTools

Identifikasi lokasi kerusakan DOM menggunakan langkah terstruktur berikut:

  1. Bandingkan Server HTML vs Rendered DOM: Buka tab Network, periksa payload dokumen asli (Response tab). Bandingkan langsung dengan tree di tab Elements. Cari tag yang posisinya dipindahkan otomatis oleh browser akibat parsing spec HTML5.
  2. Pasang Breakpoint pada Hydration Core: Jalankan breakpoint conditional pada file script framework di baris tempat error mismatch dilempar (contoh pada React/Preact: cari string peringatan did not match atau fungsi diffChildren).
  3. Inspeksi Node Mismatch: Di console, evaluasi node terkait:
    // Periksa tipe node yang berselisih
    console.log($0.nodeType, $0.nodeName, $0.textContent);
    Jika nodeType === 3, mismatch murni dipicu oleh ekstraksi TextNode dari whitespace compiler Zig.

Pola Integrasi Hydration-Safe

Untuk mengeliminasi hydration error secara permanen pada SSG berbasis Zig, gunakan pola integrasi berbasis isolasi DOM atau penundaan hidrasi.

1. Custom Elements dengan Shadow DOM (Direct Mount Hook)

Pendekatan paling aman adalah memanfaatkan Shadow DOM. Elemen statis dari Zig bertindak murni sebagai light DOM fallback, sementara interaktivitas berjalan di dalam shadow root tanpa menyentuh struktur root server-rendered.

class InteractiveCounter extends HTMLElement {
  connectedCallback() {
    // Hindari re-hydration light DOM jika shadow root digunakan
    if (!this.shadowRoot) {
      const shadow = this.attachShadow({ mode: 'open' });
      const initialCount = parseInt(this.getAttribute('data-count') || '0', 10);
      
      shadow.innerHTML = `
        <style>button { padding: 4px 8px; }</style>
        <button id="dec">-</button>
        <span id="val">${initialCount}</span>
        <button id="inc">+</button>
      `;

      let count = initialCount;
      const valEl = shadow.getElementById('val');
      shadow.getElementById('inc').addEventListener('click', () => {
        count++;
        valEl.textContent = count.toString();
      });
      shadow.getElementById('dec').addEventListener('click', () => {
        count--;
        valEl.textContent = count.toString();
      });
    }
  }
}
customElements.define('interactive-counter', InteractiveCounter);

2. Defer Hydration Pattern (Client Island Mount)

Jika menggunakan framework UI berbasis VDOM (seperti Preact atau Petite-Vue), hindari mode inline-hydration langsung pada container yang dirender Zig. Gunakan pola penggantian node atau render murni di client-side:

<!-- Output Statis dari Zine Template -->
<div id="island-container" data-component="search-bar">
  <!-- SSR fallback content tanpa whitespace acak -->
  <input type="text" placeholder="Cari...">
</div>

<script type="module">
  import { h, render } from 'preact';
  import { SearchBar } from './components/SearchBar.js';

  // Tangguhkan hidrasi sampai browser idle untuk mencegah layout shift
  requestIdleCallback(() => {
    const container = document.getElementById('island-container');
    if (container) {
      // Kosongkan container atau pasang VDOM baru tanpa hidrasi berbasis tree
      container.innerHTML = '';
      render(h(SearchBar, null), container);
    }
  });
</script>

Pendekatan penundaan dengan membersihkan kontainer (atau menimpa secara eksplisit) menjamin pohon DOM yang dihasilkan runtime JavaScript tidak berbenturan dengan anomali parser HTML statis dari kompilasi Zig.