Akar Masalah: DOM Text Node dan Inkonsistensi Whitespace
Hydration mismatch terjadi ketika struktur Virtual DOM (VDOM) yang dibuat di sisi client WebAssembly berbeda dengan struktur DOM nyata hasil render server-side (SSR) yang diparsing browser. Pada ekosistem Rust menggunakan Yew dan Actix Web, salah satu pemicu paling umum namun sulit dilacak adalah representasi whitespace dan text node.
Browser HTML parser mengonversi karakter spasi, tab, dan baris baru di antara elemen HTML menjadi instance TextNode. Jika Yew me-render markup di server secara kompak tanpa whitespace ekstra di antara tag, tetapi template HTML wrapper di Actix Web menambahkan indentasi atau baris baru di sekitar kontainer root aplikasi, browser akan memproduksi node anak yang tidak sinkron. Ketika yew::Renderer::hydrate() dijalankan di client, runtime mendeteksi ketidakcocokan jumlah atau urutan node, memicu panic atau memaksa client melakukan re-render penuh yang merusak performa.
Konfigurasi ServerRenderer pada Actix Web
Saat merender komponen Yew di server menggunakan yew::ServerRenderer, markup komponen harus dimasukkan ke dalam kerangka HTML dokumen tanpa memodifikasi spasi di batas node root.
use actix_web::{get, web, HttpResponse, Responder};
use yew::ServerRenderer;
use crate::app::App;
#[get("/")]
async fn index() -> impl Responder {
let renderer = ServerRenderer::<App>::new();
let app_html = renderer.render().await;
// PENTING: Jangan tambahkan newline atau spasi di dalam tag mount root
let full_html = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Yew SSR Actix</title>
</head>
<body><div id="app">{}</div><script type="module">import init from '/pkg/client.js'; init();</script></body>
</html>"#,
app_html
);
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(full_html)
}Perhatikan bahwa tag <div id="app">{}</div> membungkus output render secara langsung tanpa karakter baris baru. Penambahan spasi seperti <div id="app">\n {} \n</div> akan menghasilkan TextNode kosong yang langsung merusak traversal tree hidrasi Yew.
Preservasi Whitespace pada Macro html!
Makro yew::html! memiliki aturan deterministik terkait whitespace. Dua token string bersebelahan atau spasi di antara tag sering kali diabaikan atau disatukan. Jika layout Anda bergantung pada spasi eksplisit antar teks dan inline element, hindari spasi mentah pada markup.
Gunakan ekspresi literal string untuk memastikan konsistensi antara SSR HTML output dan client VDOM parser:
use yew::prelude::*;
#[function_component(InlineText)]
pub fn inline_text() -> Html {
html! {
<p>
{ "Status:" }
{ " " }
<span class="badge">{ "Aktif" }</span>
</p>
}
}Menggunakan { " " } menjamin bahwa compiler menyematkan string node statis di kedua sisi target (native runtime server dan WebAssembly client), mencegah hilangnya spasi saat minifikasi atau serialisasi.
Serialisasi State Hidrasi Menggunakan Script JSON
Mismatch tidak hanya disebabkan oleh whitespace formatting, tetapi juga ketidaksinkronan state data awal antara server dan client. Komponen yang bergantung pada query database di server harus mentransfer snapshot data tersebut ke client agar output DOM pertama di client identik dengan server output.
Bungkus state ke dalam script tag bertipe application/json:
// Di sisi Actix Web handler
let initial_data = serde_json::json!({ "user_id": 42, "role": "admin" });
let data_script = format!(
r#"<script id="__INITIAL_STATE__" type="application/json">{}</script>"#,
serde_json::to_string(&initial_data).unwrap()
);Pada sisi client Wasm, baca dan deserialisasi elemen script sebelum memanggil hydrate():
use wasm_bindgen::prelude::*;
use web_sys::{window, Element};
use yew::Renderer;
use crate::app::{App, AppProps};
#[wasm_bindgen(start)]
pub fn run_app() {
let window = window().expect("Window context missing");
let document = window.document().expect("Document missing");
let state_elem = document
.get_element_by_id("__INITIAL_STATE__")
.expect("State script missing");
let state_json = state_elem.text_content().unwrap_or_default();
let props: AppProps = serde_json::from_str(&state_json).unwrap();
let root = document.get_element_by_id("app").expect("Root container missing");
// Eksekusi hidrasi dengan props deterministik
Renderer::<App>::with_root_and_props(root, props).hydrate();
}Aturan Middleware dan Minifikasi
Jika menggunakan middleware kompresi atau transformasi HTML di Actix Web, pastikan middleware tersebut tidak memotong spasi secara agresif di dalam blok HTML text content. Minifikasi HTML yang memotong whitespace antar inline element (seperti <span> atau <a>) akan menyebabkan browser membangun tree tanpa TextNode yang diharapkan oleh client VDOM.
Gunakan minifikasi hanya pada tahap build-time aset client, atau gunakan parser HTML minifier yang mematuhi spesifikasi preservasi whitespace inline context.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!