Hydration mismatch terjadi ketika representasi DOM hasil rendering server berbeda byte-for-byte dengan struktur virtual DOM yang diinisialisasi browser pada siklus mount pertama. Pada fitur obrolan berbasis AI, input sering kali mengandung mention token seperti @agent:analyzer atau @agent[summarizer]. Jika parser mention mengandalkan runtime browser, me-resolve metadata asinkron di tengah render, atau menggunakan sanitasi DOM yang tidak identik di Node.js dan browser, framework seperti React atau Vue akan membuang DOM server dan melakukan re-render client yang memicu degradasi performa dan layout shift.

Akar Masalah Hydration Mismatch pada Mention AI

Terdapat tiga titik kegagalan utama saat memproses tag mention dalam konteks SSR:

  1. Inkonsistensi Lexer dan Resolusi Asinkron: Komponen server me-render teks mentah atau menunggu data agen dari cache lokal, sementara klien menjalankan parsing berbasis API fetch agen secara asinkron. Perbedaan state awal ini menghasilkan perbedaan output teks atau markup.
  2. Perbedaan Implementasi Parser Sanitasi DOM: Penggunaan pustaka sanitasi berbasis C++ bindings atau regex di Node.js sering kali menangani whitespace, self-closing tag, dan urutan atribut HTML secara berbeda dibandingkan DOMParser native browser.
  3. Injeksi Dynamic Metadata Prematur: Menghasilkan markup interaktif seperti popover profil agen, status badge, atau event listener langsung ke dalam tree SSR. Klien sering menginjeksi ID unik acak (seperti auto-generated UUID) untuk aksesibilitas tooltip sebelum hydration selesai, memicu tree mutation error.

Arsitektur Solusi: Pipeline AST Deterministik

Solusi teknis yang tepat memisahkan parsing sintaksis dari resolusi state interaktif. Alur kerja dibagi menjadi dua fase:

  • Fase 1 (Deterministik & Isomorfik): Teks mentah diurai menjadi Abstract Syntax Tree (AST) statis menggunakan lexer sinkron. Render AST menghasilkan HTML statis dengan atribut data-* standar tanpa ID dinamis acak.
  • Fase 2 (Progressive Hydration): Interaktivitas (tooltip, hovercard profil agen) di-mount secara bertahap via client component setelah siklus hydration utama selesai (deferred rendering).

Implementasi TypeScript: Parser Deterministik

Snippet berikut mengimplementasikan parser mention agen berbasis AST murni tanpa dependensi DOM luar, memastikan output identik di runtime Node.js dan browser.

// types.ts
export type TextNode = { type: 'text'; content: string };
export type AgentMentionNode = {
  type: 'agent_mention';
  agentId: string;
  label: string;
};
export type ASTChild = TextNode | AgentMentionNode;

// parser.ts
// Format target: @agent:[id]{Label}
const MENTION_REGEX = /@agent:\[([a-zA-Z0-9_-]+)\](?:\{([^}]+)\})?/g;

export function parseAgentMentions(rawText: string): ASTChild[] {
  const nodes: ASTChild[] = [];
  let lastIndex = 0;
  let match: RegExpExecArray | null;

  // Reset regex state
  MENTION_REGEX.lastIndex = 0;

  while ((match = MENTION_REGEX.exec(rawText)) !== null) {
    const [fullMatch, agentId, customLabel] = match;
    const matchIndex = match.index;

    // Push teks statis sebelum match
    if (matchIndex > lastIndex) {
      nodes.push({
        type: 'text',
        content: rawText.slice(lastIndex, matchIndex),
      });
    }

    nodes.push({
      type: 'agent_mention',
      agentId,
      label: customLabel || agentId,
    });

    lastIndex = matchIndex + fullMatch.length;
  }

  // Push sisa teks setelah match terakhir
  if (lastIndex < rawText.length) {
    nodes.push({
      type: 'text',
      content: rawText.slice(lastIndex),
    });
  }

  return nodes;
}

// renderer.ts
// Menghasilkan markup statis tanpa injeksi ID dinamis acak
export function renderAstToStaticHtml(nodes: ASTChild[]): string {
  return nodes
    .map((node) => {
      if (node.type === 'text') {
        return escapeHtml(node.content);
      }
      return `<span class="agent-mention" data-agent-id="${escapeHtml(node.agentId)}">@${escapeHtml(node.label)}</span>`;
    })
    .join('');
}

function escapeHtml(str: string): string {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

Isolasi Komponen Interaktif di Klien

Setelah elemen dasar ter-render via SSR, gunakan teknik deferred hydration untuk mengaktifkan UI kompleks tanpa merusak tree sinkronisasi awal:

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

interface AgentBadgeProps {
  agentId: string;
  label: string;
}

export const AgentBadge: React.FC<AgentBadgeProps> = ({ agentId, label }) => {
  const [isHydrated, setIsHydrated] = useState(false);

  useEffect(() => {
    // Flag aktif hanya setelah hydration browser tuntas
    setIsHydrated(true);
  }, []);

  // SSR dan render awal client identik
  if (!isHydrated) {
    return (
      <span className="agent-mention" data-agent-id={agentId}>
        @{label}
      </span>
    );
  }

  // Interaktivitas aktif secara aman pasca-hydration
  return (
    <span className="agent-mention interactive" data-agent-id={agentId}>
      <button onClick={() => console.log(`Trigger agent modal: ${agentId}`)}>
        @{label}
      </button>
    </span>
  );
};

Verifikasi: Runnable Assertion Test

Gunakan unit test berikut untuk memverifikasi konsistensi keluaran antara simulasi server rendering dan client render pass:

import assert from 'assert';

function testDeterministicMentionOutput() {
  const rawInput = 'Halo @agent:[code-reviewer]{Reviewer Bot}, mohon audit modul ini.';
  
  // 1. Eksekusi SSR pass
  const serverAst = parseAgentMentions(rawInput);
  const serverHtml = renderAstToStaticHtml(serverAst);

  // 2. Eksekusi Client Initial pass (sebelum useEffect)
  const clientAst = parseAgentMentions(rawInput);
  const clientHtml = renderAstToStaticHtml(clientAst);

  // 3. Verifikasi kesamaan mutlak output
  assert.strictEqual(
    serverHtml,
    clientHtml,
    'SSR HTML dan Client Initial HTML harus identik untuk mencegah hydration error.'
  );

  assert.strictEqual(
    serverHtml,
    'Halo <span class="agent-mention" data-agent-id="code-reviewer">@Reviewer Bot</span>, mohon audit modul ini.',
    'Output HTML tidak cocok dengan template yang diharapkan.'
  );

  console.log('PASS: Deterministic parser check passed successfully.');
}

testDeterministicMentionOutput();

Panduan Debugging Hydration

  • Cek Tag Nesting: Pastikan token mention tidak merender blok <div> di dalam tag inline seperti <p>. Browser akan memindahkan tag blok ke luar secara otomatis, merusak hierarki tree React.
  • Hindari Sanitizer Lintas Bahasa: Jika SSR berjalan di Node.js, gunakan regex parsing deterministik atau library berbasis WebAssembly murni daripada menggabungkan engine C++ di server dengan DOM browser.
  • CSS-First Interactivity: Gunakan pure CSS hover/focus styling untuk tooltip dasar agen bila memungkinkan, sehingga state JavaScript tidak diperlukan selama fase render pertama.