Implementasi speculative client rendering menggunakan model LLM hasil distilasi (misalnya model on-device seperti WebLLM atau quantized SLM) sering kali memicu hydration mismatch pada aplikasi SSR (Next.js, Remix). Masalah ini terjadi ketika HTML awal yang dihasilkan server menggunakan model utama (teacher/canonical) berbeda dari prediksi draft token yang dieksekusi client saat hidrasi DOM berjalan.

Akar Masalah: Mengapa Token Drift Merusak Hidrasi

Hydration mismatch terjadi ketika tree Virtual DOM React pada first-render client tidak identik dengan DOM tree hasil SSR. Pada arsitektur hybrid LLM, ada tiga pemicu utama:

1. Drift Token Boundary

Model distilasi lokal dan model server sering menggunakan tokenizer berbeda atau memiliki probabilitas distribusi token yang tidak identik. Sebagai contoh, server menghasilkan token ["Pemer", "iksaan"], sedangkan model spekulatif di client memprediksi ["Pem", "eriksaan"]. Walau hasil string akhir sama, struktur text node split di DOM pada fase streaming dapat berbeda jika chunk diinjeksikan secara reaktif sebelum hidrasi selesai.

2. Perbedaan Parsing AST Markdown

Streaming LLM menghasilkan teks parsial. Token parsial seperti ```ty atau **teks belum membentuk sintaks Markdown valid. Jika server me-render snapshot pada token $t_k$ dan client mengevaluasi Markdown parser pada token $t_{k+n}$ (atau token spekulatif yang salah), parser menghasilkan Abstract Syntax Tree (AST) yang sama sekali berbeda:

  • Server menghasilkan tag <p>**teks</p> karena belum ada penutup.
  • Client menerima penutup lebih cepat dan menghasilkan <p><strong>teks</strong></p>.

Perbedaan hirarki tag (elemen <strong> vs text node mentah) langsung memaksa React membatalkan hidrasi cepat dan melakukan full client-side remount (React Error #418 / #425).

3. Timing Desinkronisasi Stream

SSR membekukan output pada batas buffer HTTP response chunk tertentu. Begitu script hidrasi client dieksekusi di browser, thread engine lokal sudah mulai memproduksi speculative token mendahului verifikasi dari server. Perbedaan timeline ini menjamin kegagalan rekonsiliasi DOM.

Strategi Mitigasi Teknis

  1. Canonical Boundary Buffering: Jangan render token parsial secara granular ke DOM. Kumpulkan token hingga mencapai batas blok Markdown aman (misalnya: double newline \n\n atau delimiter block element).
  2. Isolasi Draft Render via Deferred Mount: Tampilkan payload kanonikal server secara utuh selama fase hidrasi. Aktifkan speculative engine client hanya setelah status hidrasi selesai menggunakan startTransition.
  3. Selektif suppressHydrationWarning: Pasang atribut ini khusus pada leaf wrapper text node streaming, bukan pada level layout root container, agar React mentoleransi perbedaan teks dinamis tanpa merusak validasi pohon DOM sekitarnya.

Implementasi Kode: SpeculativeStreamViewer

Komponen React (TypeScript) berikut mengunci render kanonikal SSR saat hidrasi dan mengalirkan token spekulatif hanya setelah tree stabil.

import React, { useState, useEffect, useTransition } from "react";

interface SpeculativeStreamViewerProps {
  initialServerText: string;
  clientTokenStream?: AsyncIterable<string>;
}

export function SpeculativeStreamViewer({
  initialServerText,
  clientTokenStream,
}: SpeculativeStreamViewerProps) {
  const [displayText, setDisplayText] = useState<string>(initialServerText);
  const [isHydrated, setIsHydrated] = useState<boolean>(false);
  const [, startTransition] = useTransition();

  useEffect(() => {
    // Flag hidrasi selesai: aman untuk menerima speculative client render
    setIsHydrated(true);

    if (!clientTokenStream) return;

    let buffer = initialServerText;
    let isSubscribed = true;

    async function consumeStream() {
      try {
        for await (const token of clientTokenStream!) {
          if (!isSubscribed) break;

          buffer += token;

          // ponytail: buffering sederhana per spasi/newline untuk cegah AST breakdown.
          // Upgrade ke parser lexer formal jika parsing markdown kompleks dibutuhkan.
          if (token.includes(" ") || token.includes("\n")) {
            const flushedText = buffer;
            startTransition(() => {
              setDisplayText(flushedText);
            });
          }
        }
      } catch (err) {
        console.error("Stream speculative processing error:", err);
      }
    }

    consumeStream();

    return () => {
      isSubscribed = false;
    };
  }, [clientTokenStream, initialServerText]);

  return (
    <div className="stream-container">
      {/* Container utama mempertahankan struktur node identik */}
      <div className="prose-output">
        <span
          suppressHydrationWarning={true}
          style={{ whiteSpace: "pre-wrap" }}
        >
          {displayText}
        </span>

        {/* Placeholder draft visual: hanya render jika sudah hydrated */}
        {isHydrated && (
          <span className="cursor-speculative" aria-hidden="true">
            ▍
          </span>
        )}
      </div>
    </div>
  );
}

Skipped: markdown AST incremental parsing engine. Add when render membutuhkan parsing inline rich text formatting (bold/table/codeblock) secara real-time.

Validasi dan Debugging

Untuk memvalidasi bahwa hydration error sudah hilang:

  • Jalankan development build (NODE_ENV=development). React mematikan peringatan hydration mismatch pada production build, yang justru menyembunyikan memory overhead akibat DOM re-creation.
  • Pastikan tidak ada log "Warning: Text content did not match. Server: ... Client: ..." pada browser console saat refresh pertama.
  • Jika menggunakan custom Markdown parser (seperti unified / remark), lakukan parse hanya pada unit blok yang telah ditutup atau batasi eksekusi AST parsing pada parent component yang telah tervalidasi isHydrated === true.