Hydration mismatch terjadi ketika struktur Virtual DOM hasil eksekusi kode di server tidak identik dengan struktur DOM riil yang dibuat oleh browser pada render pertama. Masalah ini sering muncul saat mengintegrasikan runtime WebAssembly (WASM)—seperti engine kompilasi LaTeX (misalnya latex.js, SwiftLaTeX, atau modul TeX berbasis WASM lainnya)—ke framework Server-Side Rendering (SSR) seperti Next.js atau Nuxt.

Akar Masalah: Mengapa WASM Mematahkan Hydration

Engine LaTeX WASM membutuhkan akses ke runtime browser: objek window, alokasi memori linear WebAssembly (WebAssembly.instantiate), dan sering kali manipulasi kanvas grafis atau pembuatan node SVG langsung. Pada sisi server (Node.js runtime), API ini tidak tersedia secara native atau memerlukan polifill yang berat.

Perbedaan lingkungan ini menimbulkan dua pola kerusakan:

  • DOM Desynchronization: Server merender kontainer kosong atau placeholder statis (misal teks mentah TeX \sum_{i=0}^n), sementara client langsung menggantinya dengan ratusan node SVG hasil parsing WASM sebelum hydration React/Vue selesai. React mendeteksi perbedaan atribut dan tag anak, lalu memunculkan peringatan Hydration failed because the initial UI does not match what was rendered on the server.
  • State Race Condition: Inisialisasi modul binary .wasm bersifat asinkron. Jika client mencoba me-mount komponen sebelum modul WASM siap, state lokal komponen berubah di tengah proses rekonsiliasi awal.

Solusi 1: Dynamic Import dengan SSR Disabled

Cara paling sederhana untuk menghentikan evaluasi modul di server adalah memotong jalur eksekusi SSR secara eksplisit.

Pada Next.js (App Router atau Pages Router), gunakan utilitas next/dynamic dengan opsi ssr: false. Pendekatan ini menahan komponen agar hanya diimpor dan dieksekusi setelah bundle JavaScript berjalan di browser.

// components/LatexRendererWrapper.tsx
import dynamic from 'next/dynamic';

interface LatexRendererProps {
  formula: string;
}

// Lazy alternative: MathJax/KaTeX via CSS jika tidak butuh ekosistem macro TeX lengkap.
export const LatexRenderer = dynamic(
  () => import('./LatexWasmRenderer'),
  {
    ssr: false,
    loading: () => <span className="tex-fallback">Loading TeX engine...</span>,
  }
);

Solusi 2: Pola Rendering Dua Fase (Mounted Guard)

Jika komponen pembungkus harus tetap melalui proses render server untuk struktur SEO dasar, gunakan pola two-pass rendering menggunakan hook useEffect atau flag isMounted. Kuncinya: markup server dan markup render client pertama HARUS identik.

// components/LatexWasmRenderer.tsx
'use client';

import { useState, useEffect, useRef } from 'react';

interface Props {
  source: string;
}

export default function LatexWasmRenderer({ source }: Props) {
  const [isMounted, setIsMounted] = useState(false);
  const [isCompiling, setIsCompiling] = useState(false);
  const containerRef = useRef<HTMLDivElement>(null);

  // Fase 1: Pastikan client render identik dengan server fallback
  useEffect(() => {
    setIsMounted(true);
  }, []);

  // Fase 2: Eksekusi kompilasi WASM hanya setelah DOM client terhidrasi
  useEffect(() => {
    if (!isMounted || !containerRef.current) return;

    let isCancelled = false;
    setIsCompiling(true);

    async function compileLatex() {
      try {
        // Asumsi engine diimpor secara dinamis di browser
        const { compileToSvg } = await import('@engine/latex-wasm');
        const svgOutput = await compileToSvg(source);

        if (!isCancelled && containerRef.current) {
          containerRef.current.innerHTML = svgOutput;
        }
      } catch (err) {
        console.error('Kompilasi WASM TeX gagal:', err);
      } finally {
        if (!isCancelled) setIsCompiling(false);
      }
    }

    compileLatex();

    return () => {
      isCancelled = true;
    };
  }, [isMounted, source]);

  // Server render dan client render pertama menghasilkan markup yang sama persis
  if (!isMounted) {
    return <pre className="latex-code-fallback"><code>{source}</code></pre>;
  }

  return (
    <div className="latex-container">
      {isCompiling && <span className="latex-indicator">Rendering...</span>}
      <div ref={containerRef} />
    </div>
  );
}

Solusi 3: Isolasi Kompilasi ke Web Worker

Operasi kompilasi TeX via WebAssembly memerlukan waktu CPU yang tidak instan (terutama macro parsing kompleks). Jika eksekusi WASM dilakukan di main thread UI, frame rate akan anjlok. Pisahkan inisialisasi binary WASM dan kompilasi ke instance Worker terpisah, lalu kirimkan hasilnya berupa string SVG murni ke DOM client.

// workers/latex.worker.ts
self.onmessage = async (event: MessageEvent<string>) => {
  const source = event.data;
  try {
    // Inisialisasi WASM di background thread
    const wasmModule = await import('@engine/latex-wasm');
    const resultSvg = await wasmModule.parse(source);
    self.postMessage({ ok: true, data: resultSvg });
  } catch (error) {
    self.postMessage({ ok: false, error: (error as Error).message });
  }
};
// hooks/useLatexWorker.ts
'use client';

import { useState, useEffect } from 'react';

export function useLatexWorker(source: string) {
  const [output, setOutput] = useState<string | null>(null);

  useEffect(() => {
    const worker = new Worker(new URL('../workers/latex.worker.ts', import.meta.url));

    worker.onmessage = (event) => {
      if (event.data.ok) {
        setOutput(event.data.data);
      }
    };

    worker.postMessage(source);

    return () => {
      worker.terminate();
    };
  }, [source]);

  return output;
}

Aturan Praktis Menghindari Hydration Error

  • Jangan modifikasi DOM langsung sebelum mount: Hindari pemanggilan fungsi WASM yang menerima ID elemen DOM di badan komponen (render pass). Selalu letakkan di dalam lifecycle hook (useEffect di React atau onMounted di Vue).
  • Gunakan container statis untuk output eksternal: Biarkan React hanya merender elemen kontainer kosong (misal <div ref={ref} />) dan gunakan mutasi langsung via ref untuk output WASM, atau operasikan output murni sebagai string yang disuntikkan setelah fase hydration selesai.
  • Bypass atribut bawaan: Jika perlu menampilkan teks fallback yang rentan sedikit dimodifikasi browser (misalnya normalisasi spasi pada formula), gunakan atribut suppressHydrationWarning pada tag pembungkus sebagai langkah darurat, meski pemisahan rendering tetap solusi yang paling terprediksi.