Hydration mismatch terjadi ketika representasi DOM yang dihasilkan oleh server melalui Server-Side Rendering (SSR) tidak identik dengan initial render tree yang dihitung oleh WebAssembly (WASM) di client. Pada stack Rust menggunakan leptos_actix dan Leptos, kondisi ini menyebabkan engine hydration memutus sinkronisasi reaktif, menduplikasi node, atau melakukan full fallback re-render yang merusak performa.
Akar Masalah: DOM Divergence Antara SSR dan Client
Leptos hydration bekerja secara efisien: server merender string HTML statis, browser memparsing HTML tersebut menjadi DOM, lalu binary WASM berjalan di browser dan melakukan walk pada DOM tree yang sudah ada untuk mengaitkan event listeners dan reactive signals tanpa membuat ulang node HTML.
Jika node tree hasil eksekusi awal client tidak sama persis dengan server HTML, proses traversal gagal. Penyebab paling sering adalah eksekusi data non-deterministik saat initial render:
- Nilai Dinamis Waktu/Random: Penggunaan fungsi seperti
chrono::Utc::now()atau generator UUID di dalam view template tanpa sinkronisasi state. - Akses Langsung ke Web API: Membaca
window.inner_width,localStorage, ataunavigator.user_agentyang hanya ada pada browser sehingga menghasilkan nilai berbeda antara server dan client. - Branching Logic Berdasarkan Environment: Conditional rendering
if cfg!(target_arch = "wasm32")langsung pada template view tanpa penanganan hydration fallback.
Komponen Bermasalah vs Solusi Deterministik
Contoh komponen yang memicu hydration mismatch akibat pembacaan waktu lokal saat render:
// SALAH: Memicu hydration mismatch
#[component]
pub fn ClockDisplay() -> impl IntoView {
// Server merender waktu T_server, client merender waktu T_client saat WASM start
let current_time = chrono::Local::now().format("%H:%M:%S").to_string();
view! {
<div class="clock">
<span>"Rendered at: " {current_time}</span>
</div>
}
}Solusinya adalah memisahkan initial state deterministik dengan update dinamis di browser menggunakan create_effect:
// BENAR: Initial render identik antara server dan client
use leptos::*;
#[component]
pub fn ClockDisplay() -> impl IntoView {
let (time, set_time) = create_signal(String::new());
// create_effect hanya dieksekusi di browser setelah initial hydration selesai
create_effect(move |_| {
set_time.set(chrono::Local::now().format("%H:%M:%S").to_string());
});
view! {
<div class="clock">
// SSR dan initial client render sama-sama menghasilkan teks kosong
<span>"Rendered at: " {move || time.get()}</span>
</div>
}
}Sinkronisasi Server State: create_resource dan Suspense
Ketika server mengambil data asinkron (misalnya dari database PostgreSQL via Actix context), data tersebut harus diserialisasikan ke dalam HTML response agar client langsung menggunakan data yang sama tanpa fetch ulang yang dapat menghasilkan output berbeda.
Leptos menyediakan mekanisme serialisasi otomatis menggunakan kombinasi create_resource dan <Suspense>:
use leptos::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerMetrics {
pub load_avg: String,
}
#[server(GetMetrics, "/api")]
pub async fn get_metrics() -> Result<ServerMetrics, ServerFnError> {
// Dijalankan eksklusif di Actix Web server
Ok(ServerMetrics {
load_avg: "0.42".to_string(),
})
}
#[component]
pub fn MetricsViewer() -> impl IntoView {
// Resource otomatis mendistribusikan data serial ke script tag hydration
let metrics = create_resource(|| (), |_| async move { get_metrics().await });
view! {
<Suspense fallback=move || view! { <p>"Loading metrics..."</p> }>
{move || {
metrics.get().map(|res| match res {
Ok(data) => view! { <div>"Load: " {data.load_avg}</div> }.into_view(),
Err(_) => view! { <div>"Error loading data"</div> }.into_view(),
})
}}
</Suspense>
}
}Injeksi State Context pada Handler Actix Web
Agar server functions dan Leptos template dapat mengakses layer data yang konsisten (seperti database connection pool atau application config), registrasikan state ke dalam reactive context saat request diproses oleh Actix Web.
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
use crate::app::{App, AppState};
pub async fn server_rendered_handler(
req: HttpRequest,
app_state: web::Data<AppState>,
) -> impl Responder {
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
// Injeksi AppState ke Leptos context level-request
let app_state_clone = app_state.get_ref().clone();
leptos_actix::render_app_to_stream_with_context(
leptos_options,
move || {
provide_context(app_state_clone.clone());
},
App,
)(req).await
}Isolasi Blok Render Khusus Browser
Jika memiliki komponen UI yang hanya relevan atau hanya dapat dirender dengan Web APIs (misalnya canvas charting library atau komponen yang membaca window.matchMedia), hindari render sama sekali di server.
Gunakan sinyal berbasis status client:
use leptos::*;
#[component]
pub fn ClientOnlyCanvas() -> impl IntoView {
let (is_mounted, set_mounted) = create_signal(false);
create_effect(move |_| {
set_mounted.set(true);
});
view! {
<div class="canvas-container">
{move || {
if is_mounted.get() {
view! { <canvas id="client-chart"></canvas> }.into_view()
} else {
view! { <div class="skeleton-placeholder">"Rendering..."</div> }.into_view()
}
}}
</div>
}
}Verifikasi Hydration Warning pada Console Browser
Saat hydration gagal, browser console pada build non-release (debug) akan menampilkan diagnostic warning. Di Leptos, mismatch terdeteksi ketika token DOM atau node text yang diekstrak dari server HTML tidak cocok dengan ekspektasi WebAssembly driver.
- Jalankan Cargo Leptos dalam Mode Development: Pastikan binary WASM tidak di-compile dengan profile
--releasesaat proses debugging. - Buka Browser DevTools: Periksa console log. Hydration mismatch biasanya menghasilkan pesan seperti:
hydration error: expected node [type] but found [type]atau ketidaksesuaian text content. - Breakpoint pada WASM Panic: Aktifkan Pause on caught exceptions pada DevTools untuk melacak stack trace hingga ke deklarasi komponen Rust yang memicu desinkronisasi.
- Bandingkan Output Source vs Inspect Elements: Lakukan View Page Source (HTML mentah dari Actix) dan bandingkan struktur node secara langsung dengan panel Elements (DOM tree aktif setelah WASM berjalan). Perbedaan atribut atau urutan tag child menandakan lokasi persis mismatch.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!