Penyebab Utama Hydration Mismatch pada WASM SSR

Hydration mismatch terjadi ketika Virtual DOM atau struktur state yang dihasilkan renderer server tidak identik dengan pohon komponen yang diinisialisasi modul WebAssembly (WASM) di browser. Pada arsitektur JavaScript konvensional, perbedaan ini sering dipicu oleh pemanggilan API lingkungan yang tidak sinkron (seperti window atau manipulasi waktu lokal).

Pada ekosistem WASM, ada faktor kegagalan tambahan: state drift akibat kompilasi independen (decoupled dual-target build). Server dieksekusi sebagai native binary (misal x86_64-linux), sedangkan client dikompilasi ke wasm32-freestanding atau wasm32-wasi.

Jika kedua target tersebut dikompilasi lewat skrip terpisah, perbedaan flag optimasi, konstanta feature-flagging, atau layout padding struct data dapat mengubah struktur HTML server dan ekspektasi deserialisasi binary client WASM. Akibatnya, browser membatalkan proses hidrasi (tearing) dan merender ulang seluruh DOM dari awal.

Alternatif lebih instan: Sinkronisasi konfigurasi lewat file JSON runtime tunggal. Konsekuensi: Menambah overhead parsing runtime dan menghilangkan dead-code elimination berbasis compile-time.

Pola Build Zig: Unified Dependency DAG

Pola build Zig memperlakukan seluruh pipeline sebagai graf asiklik terarah (DAG) dalam satu file eksekusi. Pola ini mengikat konfigurasi host executable dan target WASM ke dalam satu sumber kebenaran (single source of truth). Flag kompilasi, konstanta skema, dan hash artefak diikat secara atomik.

Berikut konfigurasi minimal build.zig untuk mengunci dual-target build dan menyuntikkan compile-time options yang sama ke server dan client:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // 1. Single source of truth untuk konfigurasi dan skema
    const schema_version: u32 = 4;
    const schema_signature = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

    const shared_options = b.addOptions();
    shared_options.addOption(u32, "SCHEMA_VERSION", schema_version);
    shared_options.addOption([]const u8, "SCHEMA_SIG", schema_signature);
    shared_options.addOption(bool, "ENABLE_METRICS", false);

    // 2. Client WASM target
    const wasm_target = b.resolveTargetQuery(.{
        .cpu_arch = .wasm32,
        .os_tag = .freestanding,
    });
    const client_wasm = b.addExecutable(.{
        .name = "client",
        .root_source_file = b.path("src/client.zig"),
        .target = wasm_target,
        .optimize = optimize,
    });
    client_wasm.root_module.addOptions("build_config", shared_options);

    // 3. Server Host target
    const server = b.addExecutable(.{
        .name = "server",
        .root_source_file = b.path("src/server.zig"),
        .target = target,
        .optimize = optimize,
    });
    server.root_module.addOptions("build_config", shared_options);

    // Server bergantung langsung pada artefak client WASM
    server.step.dependOn(&client_wasm.step);

    b.installArtifact(client_wasm);
    b.installArtifact(server);
}

Build script ini menjamin flag kompilasi seperti ENABLE_METRICS dan SCHEMA_SIG tidak akan pernah bergeser antara binary server dan binary client. Keduanya menerima modul build_config yang sama.

Mengunci Manifest Hydration dan Payload Schema

Untuk mencegah mismatch akibat perbedaan serialisasi data, server harus menyematkan hash signature skema ke dalam root payload DOM. Modul WASM client membaca signature tersebut sebelum menjalankan binding event.

Implementasi validasi pada client WASM (src/client.zig):

const std = @import("std");
const config = @import("build_config");

extern "env" fn js_panic(ptr: [*]const u8, len: usize) void;
extern "env" fn js_mount_app() void;

pub export fn hydrate(dom_hash_ptr: [*]const u8, dom_hash_len: usize) void {
    const server_hash = dom_hash_ptr[0..dom_hash_len];
    
    // Verifikasi identitas skema build secara deterministik
    if (!std.mem.eql(u8, server_hash, config.SCHEMA_SIG)) {
        const err_msg = "Hydration Aborted: Schema mismatch between server and client build.";
        js_panic(err_msg.ptr, err_msg.len);
        return;
    }

    // ponytail: parsing flat buffer langsung; migrasi ke zero-copy struct validator jika skema melebihi 100 field
    js_mount_app();
}

Server wajib mencetak hash tersebut ke dalam tag HTML saat initial render:

<div id="app" data-schema-sig="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855">
  <!-- Server-rendered DOM -->
</div>
<script src="/client.wasm"></script>

Handling Mismatch di Sisi Middleware

Jika hash tidak cocok akibat cache browser yang stale, client tidak boleh membiarkan DOM berada dalam kondisi setengah terhidrasi. Tambahkan guard middleware di JavaScript bootstrap bridge:

function initHydration(wasmInstance) {
  const root = document.getElementById("app");
  const serverHash = root.getAttribute("data-schema-sig");

  try {
    // Alokasikan memori WASM untuk parsing hash string
    const bytes = new TextEncoder().encode(serverHash);
    const ptr = wasmInstance.exports.alloc(bytes.length);
    const memory = new Uint8Array(wasmInstance.exports.memory.buffer);
    memory.set(bytes, ptr);

    // Jalankan entrypoint hidrasi
    wasmInstance.exports.hydrate(ptr, bytes.length);
    wasmInstance.exports.dealloc(ptr, bytes.length);
  } catch (err) {
    console.warn("State drift terdeteksi, fallback ke clean client-side mount:", err);
    // Recovery: Bersihkan server DOM untuk mencegah memory leak atau event listener yatim
    root.innerHTML = "";
    wasmInstance.exports.mount_clean_client();
  }
}

Verifikasi Integritas Otomatis

Untuk memastikan sinkronisasi artefak tidak terabaikan di CI/CD pipeline, pasang automated check pada Zig test harness. Tes ini memeriksa kesesuaian nilai konfigurasi antara target host dan client.

Tambahkan runner verifikasi ini pada tests/build_integrity_test.zig:

const std = @import("std");
const testing = std.testing;
const config = @import("build_config");

test "verifikasi integritas hash dan flag skema build" {
    // Nilai minimum hash valid (SHA-256 hex string)
    try testing.expectEqual(@as(usize, 64), config.SCHEMA_SIG.len);
    try testing.expect(config.SCHEMA_VERSION > 0);

    // Pastikan flag sensitif tidak aktif pada build rilis tanpa deklarasi eksplisit
    if (builtin.mode == .ReleaseFast or builtin.mode == .ReleaseSmall) {
        try testing.expectEqual(false, config.ENABLE_METRICS);
    }
}

Jalankan pemeriksaan via CLI:

zig build test --summary all

Dilewati: Generasi runtime dynamic AST validator. Tambahkan jika server dan WASM client di-deploy independen oleh tim terpisah.