Render mismatch pada aplikasi Rust SSR biasanya terjadi saat server merender HTML berdasarkan feature flag, cookie, tema, atau preferensi user, tetapi saat hydration di browser, klien memulai dengan state yang tidak sama. Akibatnya, struktur DOM, atribut, atau teks awal berbeda dari yang diharapkan runtime UI, lalu muncul peringatan hydration, UI berkedip, event tidak terpasang dengan benar, atau komponen dirender ulang secara paksa.

Masalah ini bukan bug acak di framework. Akar masalahnya hampir selalu sama: server dan klien tidak berbagi sumber kebenaran yang identik untuk initial state. Solusi utamanya adalah memastikan keputusan render awal dibuat dari data yang sama di kedua sisi, lalu menyalurkan state tersebut dari server ke klien secara eksplisit.

Mengapa render mismatch terjadi pada Rust SSR

Pada SSR, ada dua fase penting:

  1. Server render: server menghasilkan HTML awal berdasarkan request masuk, termasuk header, cookie, session, atau hasil evaluasi feature flag.
  2. Hydration di klien: kode Rust yang dikompilasi ke WebAssembly atau runtime klien mencoba “mengambil alih” HTML yang sudah ada.

Hydration mengasumsikan bahwa output render pertama di klien akan sama dengan HTML yang sudah dikirim server. Jika asumsi ini gagal, runtime akan mendeteksi perbedaan.

Pada kasus feature flag dan cookie, mismatch biasanya muncul karena:

  • Server membaca cookie dari request, tetapi klien memakai nilai default yang berbeda.
  • Server mengaktifkan varian A/B tertentu, tetapi klien mengevaluasi flag lagi dan mendapat hasil lain.
  • Server merender tema gelap/terang dari cookie, tetapi klien baru menentukan tema setelah startup.
  • Server memakai preferensi user dari session, sedangkan klien belum memiliki state itu saat hydration dimulai.

Gejala yang umum terlihat

Gejalanya berbeda-beda tergantung framework SSR Rust yang dipakai, tetapi polanya mirip:

  • Peringatan hydration mismatch di console browser.
  • Elemen yang semula muncul di HTML server lalu hilang setelah klien aktif.
  • Teks, class, atribut data-*, atau struktur node berubah saat startup.
  • Flash UI, misalnya tema gelap berubah ke terang lalu kembali lagi.
  • Handler event tidak bekerja pada subtree tertentu sampai komponen dirender ulang.
  • Komponen interaktif tertentu tampak “reset” pada load pertama.

Jika HTML server dan render pertama klien tidak identik, hydration menjadi rapuh. Bahkan jika perbedaannya kecil, misalnya satu class tema atau satu blok varian eksperimen, runtime tetap bisa menganggap subtree tidak sinkron.

Contoh alur request yang memicu mismatch

Kasus tema dari cookie

  1. Browser mengirim request dengan cookie theme=dark.
  2. Server membaca cookie dan merender tombol, icon, dan class halaman untuk mode gelap.
  3. HTML dikirim ke browser.
  4. Saat startup, state awal di klien dibuat dengan default theme=light karena kode klien tidak menerima nilai dari server.
  5. Hydration membandingkan render klien versi terang dengan HTML server versi gelap.
  6. Terjadi mismatch.

Kasus feature flag A/B

  1. Server mengevaluasi flag new_checkout berdasarkan cookie eksperimen atau user ID.
  2. Server merender varian B.
  3. Klien memanggil evaluator flag lagi tanpa konteks request yang sama, atau memakai fallback default varian A.
  4. Hydration gagal karena struktur checkout berbeda.

Sumber mismatch yang paling sering

1. Cookie hanya dibaca di salah satu sisi

Server punya akses alami ke header Cookie dari request. Klien baru bisa membaca document.cookie setelah runtime browser aktif, dan itupun tidak untuk cookie tertentu seperti HttpOnly. Jika render awal klien bergantung pada pembacaan cookie yang terlambat atau tidak lengkap, state awal mudah berbeda.

2. Evaluasi feature flag dilakukan dua kali dengan konteks berbeda

Feature flag sering melibatkan context seperti user ID, session, negara, perangkat, atau assignment eksperimen. Jika server dan klien tidak memakai input identik, hasil evaluasi bisa berubah. Ini sangat berbahaya bila flag mengubah struktur markup, bukan hanya perilaku minor.

3. Preferensi user dimuat asinkron di klien

Server sudah tahu preferensi dari session atau request context, tetapi klien baru mengambilnya dari endpoint setelah hydration mulai. Selama jeda itu, klien merender fallback yang berbeda dari HTML server.

4. Akses API browser saat fase render awal

Kode seperti pembacaan window, document, matchMedia, atau API browser lain pada jalur render awal dapat menghasilkan state yang tidak tersedia saat SSR. Walaupun bukan penyebab tunggal, ini sering memperparah perbedaan antara server dan klien.

Prinsip utama perbaikan: satu initial state untuk dua sisi

Pola yang paling aman adalah:

  1. Server membaca semua input yang memengaruhi HTML awal: cookie, session, flag, preferensi user.
  2. Server membentuk initial state terstruktur.
  3. Server menggunakan state itu untuk merender HTML.
  4. State yang sama diserialisasi ke HTML agar bisa dibaca klien.
  5. Klien memakai state tersebut sebagai sumber kebenaran saat hydration, bukan menghitung ulang dari nol.

Dengan pola ini, server dan klien tidak perlu “menebak” hasil yang sama. Keduanya memang memulai dari data yang sama.

Strategi implementasi yang praktis

1. Bentuk state awal di server

Jangan biarkan komponen utama mengambil keputusan awal langsung dari API browser atau evaluator flag terpisah. Bangun satu objek state yang eksplisit. Misalnya:

#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
struct InitialUiState {
    theme: String,
    show_new_checkout: bool,
    user_segment: Option<String>,
}

fn build_initial_ui_state(req: &RequestContext) -> InitialUiState {
    let theme = req.cookie("theme").unwrap_or("light").to_string();
    let show_new_checkout = req.flags().is_enabled("new_checkout");
    let user_segment = req.user().and_then(|u| u.segment.clone());

    InitialUiState {
        theme,
        show_new_checkout,
        user_segment,
    }
}

Nama tipe dan API request context tentu akan berbeda per framework, tetapi idenya sama: semua keputusan render awal dikumpulkan di server lebih dulu.

2. Serialisasi state ke HTML

State yang dipakai saat SSR perlu tersedia untuk bootstrap klien. Cara umum yang framework-agnostic adalah menyisipkan JSON ke dalam HTML:

<script id="__INITIAL_STATE__" type="application/json">
{"theme":"dark","show_new_checkout":true,"user_segment":"beta"}
</script>

Lalu klien membaca dan melakukan deserialisasi sebelum hydration:

fn read_initial_state_from_dom() -> Option<InitialUiState> {
    let document = web_sys::window()?.document()?;
    let el = document.get_element_by_id("__INITIAL_STATE__")?;
    let json = el.text_content()?;
    serde_json::from_str(&json).ok()
}

Jika state dipakai untuk render pertama klien, hasil render akan sama dengan HTML server selama datanya identik.

Catatan keamanan: jika data dimasukkan ke dalam HTML, lakukan serialisasi dan escaping dengan benar. Jangan menyisipkan string mentah yang bisa memutus tag <script>. Bila berisi data sensitif, pertimbangkan apakah data itu memang perlu dikirim ke klien.

3. Gunakan initial state sebagai input root app

Kesalahan umum adalah sudah mengirim state dari server, tetapi komponen root tetap membuat state default yang berbeda. Pastikan state bootstrap benar-benar menjadi input render awal:

fn app(initial: InitialUiState) -> View {
    if initial.show_new_checkout {
        render_new_checkout(&initial.theme)
    } else {
        render_old_checkout(&initial.theme)
    }
}

Setelah hydration selesai, barulah Anda boleh menjalankan sinkronisasi lanjutan jika dibutuhkan, misalnya refresh feature flag dari service eksternal. Namun, jangan ubah keputusan struktural sebelum hydration stabil kecuali memang siap menerima rerender penuh.

4. Pilih fallback yang aman bila state belum tersedia

Dalam beberapa arsitektur, state tidak selalu bisa dihitung penuh di awal. Jika demikian, gunakan fallback yang tidak mengubah struktur markup inti. Contohnya:

  • Tampilkan placeholder yang sama di server dan klien.
  • Jangan memilih varian A atau B sebelum state tersedia; render shell netral.
  • Batasi perbedaan pada teks kecil atau atribut non-kritis, bukan subtree besar.

Trade-off-nya, Anda mungkin kehilangan personalisasi langsung pada first paint. Namun ini lebih aman daripada mengirim HTML varian tertentu lalu menggantinya saat hydration.

5. Tambahkan guard untuk API browser

Pada aplikasi SSR, kode render awal harus aman dijalankan tanpa lingkungan browser. Karena itu, semua akses ke window, document, media query, atau API serupa perlu dijaga.

fn browser_cookie(name: &str) -> Option<String> {
    let window = web_sys::window()?;
    let document = window.document()?;
    let cookie_str = document.cookie().ok()?;

    cookie_str
        .split(';')
        .map(|s| s.trim())
        .find_map(|pair| {
            let (k, v) = pair.split_once('=')?;
            if k == name { Some(v.to_string()) } else { None }
        })
}

Namun guard ini bukan solusi utama untuk mismatch. Guard hanya mencegah error saat SSR. Untuk konsistensi hydration, nilai yang dipakai render awal tetap harus berasal dari initial state server, bukan dari pembacaan browser yang terlambat.

Pola yang sebaiknya dihindari

Menghitung ulang feature flag di klien untuk render pertama

Jika flag memengaruhi struktur DOM, hindari evaluasi ulang sebelum hydration selesai. Lebih aman mewariskan hasil evaluasi server ke klien, lalu jika perlu lakukan refresh setelah app hidup.

Menyediakan default yang berbeda dari keputusan server

Contoh paling umum: server merender berdasarkan cookie, tetapi klien selalu mulai dari default false, light, atau varian kontrol. Ini hampir pasti memicu mismatch.

Mencampur sumber kebenaran

Misalnya tema ditentukan server dari cookie, tetapi komponen tertentu membaca tema dari context default di klien. Atau halaman checkout memakai hasil flag server, tetapi banner eksperimen mengevaluasi sendiri di klien. Pisahkan dengan jelas: untuk render awal, gunakan satu state bootstrap yang konsisten.

Contoh arsitektur alur yang lebih aman

Berikut pola umum yang cocok untuk banyak framework UI Rust berbasis SSR:

  1. HTTP request masuk ke server SSR.
  2. Middleware atau request handler membaca cookie, session, user, dan context flag.
  3. Server membangun InitialUiState.
  4. SSR render memakai InitialUiState.
  5. InitialUiState diserialisasi ke HTML.
  6. Bootstrap klien membaca InitialUiState dari HTML.
  7. Hydration memakai state yang sama.
  8. Sinkronisasi pasca-hydration opsional untuk data dinamis yang tidak memengaruhi markup awal.

Pola ini membuat personalisasi awal tetap mungkin tanpa mengorbankan konsistensi SSR.

Trade-off dan batasan

Serialisasi state menambah payload HTML

Semakin banyak state yang Anda kirim, semakin besar HTML awal. Karena itu, kirim hanya data yang benar-benar dibutuhkan untuk render dan hydration awal.

Tidak semua cookie aman dikirim ke klien

Cookie atau hasil turunan yang sensitif tidak selalu boleh diekspos. Dalam kasus seperti itu, kirim hanya nilai minimal yang dibutuhkan UI, misalnya show_new_checkout: true, bukan seluruh konteks evaluasi atau metadata eksperimen.

Feature flag dinamis bisa berubah setelah HTML dibuat

Ada kemungkinan state di server berubah beberapa saat kemudian. Untuk fase hydration, yang penting bukan “paling baru”, melainkan konsisten. Lebih baik state sedikit stale tetapi sama di dua sisi daripada “lebih baru” namun memicu mismatch pada startup.

Checklist debugging render mismatch

Jika Anda mengalami mismatch pada Rust SSR, periksa hal berikut secara sistematis:

  1. Bandingkan keputusan render awal server dan klien. Apakah flag, tema, atau preferensi yang dipakai sama?
  2. Log initial state di server. Simpan nilai yang dipakai saat SSR untuk request tertentu.
  3. Log bootstrap state di klien. Pastikan hasil deserialisasi dari HTML identik dengan state server.
  4. Cari default state yang tersembunyi. Periksa context, store, hook, atau constructor komponen yang mungkin membuat nilai bawaan berbeda.
  5. Audit akses API browser. Pastikan render awal tidak bergantung pada window atau document.
  6. Periksa apakah feature flag dievaluasi ulang. Jika ya, cek apakah konteks evaluasinya sama persis.
  7. Bandingkan HTML output untuk dua varian. Jika perbedaannya struktural, mismatch akan lebih keras daripada sekadar beda class.
  8. Uji dengan cookie dan user segment yang berbeda. Banyak bug hanya muncul pada kombinasi request tertentu.
  9. Matikan personalisasi sementara. Jika mismatch hilang, berarti sumber masalah memang berasal dari state request-specific.

Rekomendasi praktis

Untuk mencegah render mismatch pada Rust SSR dari feature flag dan cookie, gunakan aturan sederhana ini:

  • Semua input yang memengaruhi HTML awal harus diputuskan di server.
  • Hasil keputusan itu harus diserialisasi ke HTML dan dipakai kembali oleh klien.
  • Render pertama klien tidak boleh memakai default yang berbeda.
  • Akses API browser harus dijaga dan tidak menjadi sumber state awal utama.
  • Jika state belum pasti, pilih fallback netral yang sama di dua sisi.

Dengan pola tersebut, Anda tidak bergantung pada perilaku spesifik satu framework UI Rust. Prinsipnya berlaku umum untuk SSR: hydration hanya stabil jika server dan klien memulai dari state yang sama.