Pada arsitektur Server-Side Rendering (SSR) berbasis Rust menggunakan Leptos, hydration adalah proses di mana modul WebAssembly (WASM) mengambil alih markup HTML statis yang dikirimkan oleh server backend (seperti Axum atau Actix-web). Modul WASM menelusuri hierarki DOM yang ada dan merekatkan event listener serta reaktivitas signal tanpa merender ulang elemen dari nol. Masalah kritis muncul ketika pohon DOM yang dihasilkan di sisi klien tidak identik dengan HTML server. Fenomena ini memicu hydration mismatch dan DOM drift.

Akar Masalah: Desinkronisasi State dan DOM Walker

Leptos memanfaatkan pendekatan reaktivitas halus (fine-grained reactivity). Saat melakukan hydration, klien menjalankan fungsi komponen untuk merekonstruksi grafik sinyal. Untuk mencocokkan node di DOM dengan representasi reaktifnya, Leptos menggunakan penanda internal berbasis komentar HTML (misalnya <!--hk=...-->) dan penelusur DOM (DOM walker).

Desinkronisasi terjadi akibat perbedaan kondisi awal antara server dan modul WASM saat bootstrapping:

  • Keterlambatan Deserialisasi State: Jika server merender komponen berdasarkan data asinkron tetapi klien memulai eksekusi sebelum payload state tersebut terdeserialisasi ke dalam konteks aplikasi, klien akan merender state awal (kosong/default), menghasilkan struktur elemen yang berbeda.
  • State Non-Deterministik: Penggunaan data dinamis sisi klien langsung di badan komponen, seperti pembacaan waktu saat ini (Instant::now()), UUID acak, atau akses API browser seperti window().inner_width(), menghasilkan nilai yang berbeda antara kompilasi native server dan runtime WASM klien.
  • Struktur Percabangan yang Bergeser: Jika sebuah blok kondisional mengevaluasi true di server dan false di klien saat komponen pertama kali dijalankan, indeks node penelusur DOM bergeser (DOM drift).

Diagnosis: Menangani Warning dan Panic wasm-bindgen

Ketika hydration gagal mempertahankan sinkronisasi struktur, mesin runtime Leptos dan lapisan wasm-bindgen akan memicu anomali teknis yang dapat dipantau langsung melalui Developer Tools peramban.

1. Peringatan Konsol Leptos

Pada mode debug, Leptos memvalidasi penanda hydration. Jika node berikutnya pada DOM walker tidak sesuai dengan ekspektasi hierarki komponen, konsol akan memunculkan peringatan diskrepansi:

hydration warning: expected dynamic node at path [0, 1, 3], but found Node::Comment

2. Runtime Panic pada wasm-bindgen

Jika drift cukup parah, WASM mencoba melakukan type casting atau dereferensi terhadap referensi pointer DOM yang mengarah ke null. Hal ini menghasilkan panic Rust yang ditangkap oleh console_error_panic_hook:

panicked at 'called `Option::unwrap()` on a `None` value', .../leptos_dom/src/hydration.rs
wasm-bindgen: imported JS function that was not marked as `catch` threw an exception:
TypeError: Cannot read properties of null (reading 'insertBefore')

Dampak langsung dari kesalahan ini adalah kegagalan interaktivitas: tombol berhenti merespons klik, input form kehilangan reaktivitas, atau elemen baru disisipkan pada lokasi yang salah di dalam pohon HTML.

Solusi Praktis: State Bootstrapping Deterministik

Untuk mengeliminasi hydration mismatch, seluruh state awal yang menentukan struktur HTML harus ditransfer dari server ke modul WASM secara atomik dan deterministik.

1. Isolasi Data Asinkron via Resource dan Suspense

Jangan membaca data asinkron tanpa perlindungan state hydration. Gunakan Resource bersama komponen <Suspense>. Fitur serialisasi bawaan Leptos memastikan hasil dari Resource di server diinjeksi ke dalam dokumen HTML sebagai data serial JSON, sehingga WASM dapat membaca cache tersebut tanpa memicu request ulang atau perubahan DOM mendadak.

use leptos::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DashboardData {
    pub user_count: u32,
    pub system_status: String,
}

#[server(GetDashboardData, "/api")]
pub async fn get_dashboard_data() -> Result<DashboardData, ServerFnError> {
    // ponytail: simplifikasi query database langsung
    Ok(DashboardData {
        user_count: 1024,
        system_status: "Operational".to_string(),
    })
}

#[component]
pub fn Dashboard() -> impl IntoView {
    let data_resource = create_resource(|| (), |_| async move { get_dashboard_data().await });

    view! {
        <div class="container">
            <h2>"Status Sistem"</h2>
            <Suspense fallback=move || view! { <p class="skeleton">"Memuat data..."</p> }>
                {move || {
                    data_resource.get().map(|res| match res {
                        Ok(data) => view! {
                            <div class="metrics">
                                <span>"Total Pengguna: " {data.user_count}</span>
                                <span>"Status: " {data.system_status}</span>
                            </div>
                        },
                        Err(_) => view! {
                            <div class="error">"Gagal memuat metrik."</div>
                        }
                    })
                }}
            </Suspense>
        </div>
    }
}

Catatan: Komponen <Suspense> menjamin server menyajikan HTML lengkap (atau streaming boundary yang valid), dan modul WASM tidak akan mencoba menimpa node fallback saat data cache tersedia pada state bootstrap.

2. Menjaga Isolasi Browser API

Hindari evaluasi langsung kondisi visual berdasarkan objek browser global (seperti window()) di luar hook create_effect. Jika sebuah elemen hanya dapat dirender di browser, cegah server merendernya secara parsial.

#[component]
pub fn ClientTimeDisplay() -> impl IntoView {
    let (client_time, set_client_time) = create_signal(None);

    // Eksekusi hanya berjalan di browser pasca-hydration
    create_effect(move |_| {
        let time_str = js_sys::Date::new_0().to_locale_time_string("id-ID");
        set_client_time.set(Some(time_str.as_string().unwrap_or_default()));
    });

    view! {
        <div class="time-box">
            {move || client_time.get().unwrap_or_else(|| "--:--:--".to_string())}
        </div>
    }
}

Validasi: Uji Verifikasi Stabilitas DOM

Untuk memverifikasi bahwa state bootstrapping tidak menimbulkan DOM drift, lakukan pengujian integrasi berbasis headless browser. Langkah ini memastikan bahwa struktur HTML saat initial response tidak mengalami regenerasi node atau pergeseran hierarki setelah skrip WASM selesai dieksekusi.

Berikut contoh skrip verifikasi otomatis menggunakan skrip Node.js (Playwright) untuk mendeteksi mutasi node DOM yang tidak diinginkan:

const { chromium } = require('playwright');
const assert = require('assert');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  let hasHydrationWarning = false;
  page.on('console', msg => {
    if (msg.type() === 'warning' && msg.text().includes('hydration')) {
      hasHydrationWarning = true;
    }
  });

  // Ambil HTML mentah dari server
  const response = await page.goto('http://127.0.0.1:3000/dashboard', {
    waitUntil: 'commit'
  });
  const ssrHtml = await response.text();

  // Tunggu modul WebAssembly menyelesaikan bootstrapping
  await page.waitForLoadState('networkidle');

  // Ambil struktur DOM setelah eksekusi WASM
  const clientHtml = await page.content();

  // Verifikasi tidak ada warning dan penanda hydration valid
  assert.strictEqual(hasHydrationWarning, false, 'Terdeteksi hydration mismatch di console!');
  assert.ok(clientHtml.includes('Total Pengguna: 1024'), 'State server tidak terikat sempurna di DOM klien');

  console.log('Verifikasi stabil: Tidak ada DOM drift terdeteksi.');
  await browser.close();
})();

Dengan memastikan payload state asinkron diinjeksikan secara deterministik via Resource dan mengisolasi API spesifik browser di luar jalur perenderan awal, integrasi modul WASM Leptos dapat melakukan hydration secara presisi tanpa degradasi performa atau eror pada runtime.