Penyebab Hydration Mismatch pada Stream Log Agent Otonom
Dashboard monitoring untuk AI agent otonom sering kali menampilkan output eksekusi terminal secara real-time. Ketika log dieksekusi di server dan di-render via Next.js atau framework React SSR lainnya, error Hydration failed because the initial UI does not match what was rendered on the server kerap muncul. Masalah ini dipicu oleh tiga faktor utama saat menangani output loopback agent atau live runtime log:
- Perbedaan Format Timestamp: Server merender string tanggal dengan locale environment container (misal UTC), sedangkan browser klien memformatnya ulang menggunakan timezone lokal pengguna.
- ANSI Escape Sequence Parsing: Terminal virtual memancarkan kode warna ANSI (seperti
\u001b[32m). Library parser klien sering menghasilkan struktur DOM berbeda dibanding parser parser string server murni. - Streaming Chunk Tak Sinkron: Agent yang aktif terus memuntahkan buffer log baru melalui WebSocket/SSE saat proses rehidrasi React sedang berjalan, sehingga state Virtual DOM klien mendahului HTML hasil SSR server.
Pemisahan Hydration Boundary dan Arsitektur Render
Solusi paling stabil bukan mematikan SSR secara global, melainkan membatasi proses rendering log terminal pada client-only boundary yang terisolasi. HTML awal dari server hanya menyediakan container terminal statis (skeleton/placeholder) untuk mencegah Cumulative Layout Shift (CLS), sementara stream log aktual ditangani sepenuhnya oleh React lifecycle di sisi klien.
Komponen Client: LogTerminal.tsx
'use client';
import { useState, useEffect, useRef } from 'react';
interface TerminalProps {
initialLogs?: string[];
streamUrl: string;
}
export default function LogTerminal({ initialLogs = [], streamUrl }: TerminalProps) {
const [logs, setLogs] = useState<string[]>(initialLogs);
const [mounted, setMounted] = useState(false);
const terminalEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setMounted(true);
const eventSource = new EventSource(streamUrl);
eventSource.onmessage = (event) => {
setLogs((prev) => [...prev, event.data]);
};
return () => {
eventSource.close();
};
}, [streamUrl]);
useEffect(() => {
if (mounted) {
terminalEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [logs, mounted]);
// Hindari mismatch dengan merender DOM terminal konsisten
return (
<div className="bg-neutral-950 font-mono text-xs text-neutral-200 p-4 rounded h-96 overflow-y-auto">
{!mounted ? (
<div className="text-neutral-500">Menghubungkan ke runtime agent...</div>
) : (
logs.map((line, idx) => (
<div key={idx} className="whitespace-pre-wrap leading-relaxed">
{line}
</div>
))
)}
<div ref={terminalEndRef} />
</div>
);
}Normalisasi Timestamp dan Sanitasi ANSI
Jika server tetap harus merender sebagian snapshot log terakhir untuk keperluan SEO atau immediate paint, konversi timestamp harus dinormalisasi menjadi Unix epoch atau string ISO deterministik sebelum output dikirim ke HTML. Jangan gunakan pemanggilan runtime seperti new Date().toLocaleTimeString() di dalam komponen render.
// Utility sanitasi deterministik
export function sanitizeLogLine(rawText: string, timestampEpoch: number): string {
// 1. Strip atau parse ANSI escape code secara konsisten
const cleanText = rawText.replace(/\u001b\[[0-9;]*m/g, '');
// 2. Format waktu deterministik menggunakan UTC eksplisit
const date = new Date(timestampEpoch);
const timeString = date.toISOString().slice(11, 19);
return `[${timeString} UTC] ${cleanText}`;
}Mencegah Layout Shift dengan Dynamic Import
Jika library terminal seperti Xterm.js digunakan untuk merender buffer agent, dynamic import dengan opsi SSR nonaktif adalah pendekatan paling aman. Pola ini memastikan wrapper tetap memegang dimensi layout yang persisten.
import dynamic from 'next/dynamic';
const DynamicTerminal = dynamic(
() => import('@/components/AgentTerminalView'),
{
ssr: false,
loading: () => (
<div className="h-96 w-full bg-neutral-950 border border-neutral-800 animate-pulse rounded" />
),
}
);
export default function AgentMonitoringPage() {
return (
<main className="p-6 space-y-4">
<h1 className="text-lg font-bold">Autonomous Loopback Agent Log</h1>
<DynamicTerminal streamUrl="/api/agent/stream" />
</main>
);
}Trade-offs dan Tips Debugging
- Dynamic Import vs Client Mounted Flag: Dynamic import (
ssr: false) memisahkan chunk JavaScript terminal dari bundle server, menghemat parsing size server namun menunda TTI (Time to Interactive). Pendekatanmountedflag mempertahankan rendering bundle terpadu tetapi membutuhkan skeleton eksplisit. - Debugging Mismatch: Di Next.js dev mode, amati badge hydration diff. Jika error merujuk pada whitespace atau text node berbeda, periksa parser ANSI yang menyuntikkan tag
<span>warna secara tidak seragam antara Node.js runtime dan browser DOM.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!