Hydration mismatch merupakan anomali umum saat mengimplementasikan Server-Side Rendering (SSR) pada framework modern, termasuk pada ekosistem fullstack Rust. Masalah ini terjadi ketika tree DOM statis yang di-render oleh backend Actix Web tidak cocok dengan Virtual DOM (VDOM) awal yang dihitung oleh Dioxus pada klien WebAssembly (WASM). Akibatnya, runtime klien terpaksa membuang nodus yang ada atau memicu full re-render, yang merusak performa serta menyebabkan flickering.
Akar Penyebab Hydration Mismatch pada Rust Fullstack
Desinkronisasi DOM antara Actix Web dan klien Dioxus umumnya bersumber dari tiga faktor utama:
- Initial State Tidak Sinkron: Server me-render komponen menggunakan data aktual (misalnya dari database), sementara klien WASM melakukan inisialisasi menggunakan
Default::default()atau state kosong karena data tidak dioper secara eksplisit. - Evaluasi Non-Deterministik: Penggunaan fungsi seperti
chrono::Utc::now(), generator UUID, atau dependensi lingkungan server (seperti HTTP header lokal) langsung di dalam badan render komponen. Perbedaan waktu eksekusi milidetik antara server dan browser akan menghasilkan output teks atau atribut yang berbeda. - Ketidaklengkapan Serialisasi Serde: Penggunaan atribut
#[serde(skip)]atau field opsional yang menghasilkan representasi berbeda saat struct diubah ke JSON dan dibaca kembali oleh WASM.
Metode Debugging di Browser Console
Saat terjadi mismatch, browser console sering kali menampilkan warning bahwa atribut atau text node tidak sesuai. Langkah isolasi masalah meliputi:
- Aktifkan
console_error_panic_hookdan logger pada klien WASM (misalnyawasm_logger::init(wasm_logger::Config::default());) untuk menangkap tracing panic saat proses hidrasi. - Bandingkan payload raw HTML yang dikirim Actix Web (melalui tab Network > Response) dengan DOM tree yang dihasilkan di tab Elements.
- Cari perbedaan pada nodus spesifik: atribut seperti
class, conditional elements (misal tag<span>yang hanya muncul saat logged in), atau whitespace pada text node.
Serialisasi dan Injeksi Payload di Actix Web
Solusi arsitektural untuk mencegah mismatch adalah merender komponen di backend dengan initial state tertentu, lalu menyematkan state yang sama persis dalam format JSON ke dalam dokumen HTML menggunakan tag <script type="application/json">.
Definisikan shared struct yang mengimplementasikan Serialize dan Deserialize:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct AppProps {
pub post_id: u32,
pub title: String,
pub rendered_at: String, // String ISO deterministik, bukan raw timestamp dinamis
}
Implementasikan handler pada Actix Web untuk me-render Dioxus VDOM dan menyematkan state JSON:
use actix_web::{get, web, HttpResponse, Responder};
use dioxus::prelude::*;
use crate::shared::{App, AppProps};
#[get("/post/{id}")]
pub async fn render_post(path: web::Path<u32>) -> impl Responder {
let post_id = path.into_inner();
// Siapkan data deterministik
let props = AppProps {
post_id,
title: "Tutorial Hydration Rust".to_string(),
rendered_at: "2024-01-01T00:00:00Z".to_string(),
};
// Render komponen Dioxus ke HTML string
let mut vdom = VirtualDom::new_with_props(App, props.clone());
vdom.rebuild_in_place();
let body_html = dioxus_ssr::render(&vdom);
// Serialisasi props untuk payload hidrasi klien
let serialized_state = serde_json::to_string(&props).unwrap_or_default();
let full_html = format!(
r#"<!DOCTYPE html>
<html>
<head><title>SSR App</title></head>
<body>
<div id="main">{}</div>
<script id="app-state" type="application/json">{}</script>
<script type="module" src="/pkg/client.js"></script>
</body>
</html>"#,
body_html, serialized_state
);
HttpResponse::Ok().content_type("text/html; charset=utf-8").body(full_html)
}
Menerima State pada Entry Point Klien Dioxus
Pada target WASM, aplikasi membaca payload JSON dari DOM sebelum Virtual DOM diinisialisasi. Dengan memasukkan struct props yang sama ke konfigurasi peluncuran Dioxus, hidrasi berlangsung deterministik tanpa modifikasi nodus DOM.
use dioxus::prelude::*;
use web_sys::window;
use crate::shared::{App, AppProps};
fn main() {
console_error_panic_hook::set_once();
// Ekstraksi initial state dari tag script HTML
let initial_props: AppProps = window()
.and_then(|w| w.document())
.and_then(|doc| doc.get_element_by_id("app-state"))
.and_then(|el| el.text_content())
.and_then(|json| serde_json::from_str(&json).ok())
.expect("Gagal membaca serialized state untuk hidrasi");
// Launch aplikasi dengan mode hydration aktif
dioxus_web::launch_with_props(
App,
initial_props,
dioxus_web::Config::new().hydrate(true)
);
}
Catatan: Hindari pemanggilan logika non-deterministik di level root komponen. Operasikan logika dinamis sisi klien (sepertiuse_futureatauuse_effect) hanya setelah hidrasi awal selesai guna mempertahankan keselarasan VDOM dengan initial state dari server.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!