Membangun web client untuk protokol display seperti X11 (misalnya implementasi berbasis yserver, noVNC, atau Broadway backend) menggunakan framework modern seperti Next.js atau React sering kali memicu hydration error. Masalah utama muncul ketika DOM tree hasil SSR mencoba mencocokkan struktur dengan state dinamis dari X11 socket yang menginisialisasi HTML5 Canvas atau konteks WebGL sebelum tree sepenuhnya terhidrasi di sisi browser.

Akar Masalah: Asinkronitas Server State dan Framebuffer Canvas

Protokol X11 beroperasi secara asynchronous berbasis event queue. Server X11 mengatur window tree, window hierarchy, dan buffer dimensions. Ketika halaman web dirender melalui SSR:

  • Server web tidak memiliki akses ke runtime grafis (seperti GPU, display driver, atau X11 socket aktif client).
  • SSR merender fallback placeholder statis (misalnya container dimensi default 0x0 atau skeleton loader).
  • Di sisi client, skrip langsung membuka WebSocket ke X-server bridge, menerima metadata window pertama (misalnya ConfigureNotify atau CreateNotify), dan memperbarui ukuran canvas secara imperatif.

Ketidakcocokan atribut width, height, atau struktur DOM internal canvas antara hasil server-render dan state awal X11 client inilah yang memicu peringatan Text content does not match server-rendered HTML atau mismatch atribut node.

Lifecycle Hook Framebuffer dan Two-Pass Rendering

HTML5 Canvas dan WebGL context adalah stateful imperative API, sedangkan React beroperasi secara deklaratif. Jangan menginisialisasi canvas context atau window listener langsung di dalam render path komponen. Gunakan pattern two-pass rendering untuk menunda rendering surface canvas hingga hydration selesai sepenuhnya.

Contoh Implementasi Hydration Guard

Berikut adalah implementasi minimal komponen viewer X11 dengan hydration guard dan penanganan state framebuffer di Next.js/React:

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

interface X11Dimensions {
  width: number;
  height: number;
}

export function X11CanvasViewer({ initialWidth = 1024, initialHeight = 768 }: { initialWidth?: number; initialHeight?: number }) {
  const [mounted, setMounted] = useState(false);
  const [dimensions, setDimensions] = useState<X11Dimensions>({ width: initialWidth, height: initialHeight });
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const wsRef = useRef<WebSocket | null>(null);

  // Pass 1: Identifikasi client mount
  useEffect(() => {
    setMounted(true);
  }, []);

  // Pass 2: Inisialisasi koneksi X11 bridge setelah DOM terhidrasi
  useEffect(() => {
    if (!mounted) return;

    const ws = new WebSocket('ws://localhost:6080/x11');
    ws.binaryType = 'arraybuffer';
    wsRef.current = ws;

    ws.onmessage = (event) => {
      const data = new Uint8Array(event.data);
      const opcode = data[0];

      // Handle X11 ConfigureNotify atau window resize (Opcode contoh)
      if (opcode === 0x16) {
        const view = new DataView(data.buffer);
        const newWidth = view.getUint16(8, true);
        const newHeight = view.getUint16(10, true);
        
        // Mutasi canvas langsung untuk performa, hindari re-render reaktif berlebih
        if (canvasRef.current) {
          canvasRef.current.width = newWidth;
          canvasRef.current.height = newHeight;
        }
        setDimensions({ width: newWidth, height: newHeight });
      }
    };

    return () => {
      ws.close();
    };
  }, [mounted]);

  // SSR Fallback untuk mencegah hydration mismatch
  if (!mounted) {
    return (
      <div 
        style={{ width: initialWidth, height: initialHeight, backgroundColor: '#000' }} 
        aria-label="X11 Display Initializing"
      />
    );
  }

  return (
    <div style={{ width: dimensions.width, height: dimensions.height, position: 'relative' }}>
      <canvas
        ref={canvasRef}
        width={dimensions.width}
        height={dimensions.height}
        style={{ display: 'block' }}
      />
    </div>
  );
}

Pencegahan Cumulative Layout Shift (CLS) saat Initial Damage Event

Pada arsitektur X11/Xorg, display server mengirimkan Damage event (melalui extension XDamage) ketika area buffer tertentu perlu digambar ulang. Jika ukuran canvas berubah drastis saat frame pertama diterima, layout browser akan bergeser (Layout Shift).

Strategi untuk menjaga stabilitas layout:

  1. Fixed Aspect Ratio Container: Kunci rasio canvas menggunakan CSS aspect-ratio atau wrapper absolut. Render container SSR dengan ukuran target screen resolution standar protokol (misalnya 1024x768 atau 1920x1080).
  2. Offscreen Buffer Swap: Simpan render buffer X11 pada OffscreenCanvas sebelum di-blit ke canvas utama di DOM. Jangan resize canvas live sampai framebuffer pertama tervalidasi.
  3. Bypass React State untuk Damage Blitting: Jangan gunakan React state untuk menampung window pixel buffer. Panggil ctx.putImageData() atau gl.texSubImage2D() langsung di dalam listener socket untuk menjaga frame rate 60 FPS tanpa overhead rekonsiliasi VDOM.
Catatan: Jika memakai Next.js App Router, Anda juga dapat membungkus viewer menggunakan dynamic import dengan opsi ssr: false untuk mengisolasi runtime browser sepenuhnya dari render pipeline server.