Hydration error pada antarmuka galeri generative AI sering terjadi ketika markup HTML hasil Server-Side Rendering (SSR) berbeda dengan Virtual DOM yang dibuat pada render pertama di client. Masalah ini lazim muncul pada aplikasi yang menampilkan output model difusi lokal seperti ideogram-oss/ideogram4.

Akar Masalah Hydration Mismatch pada Galeri AI

Penyebab utama diskrepansi DOM ini terbagi dalam tiga faktor teknis:

  • Kalkulasi Dimensi Dinamis di Client: Server me-render placeholder statis atau dimensi fallback (misal 1:1), sementara client langsung membaca metadata gambar dari JavaScript dan menyuntikkan inline style width, height, atau aspect-ratio baru sebelum hydration selesai.
  • Atribut Blob URL Non-Deterministik: Penggunaan URL.createObjectURL(blob) menghasilkan string acak unik pada setiap instance runtime. Nilai src yang berbeda antara server dan client memicu hydration mismatch instan.
  • Race Condition Metadata PyTorch Backend: Pipeline inferensi Python/PyTorch mengembalikan file gambar terpisah dari metadata tensor (resolusi variatif hasil bucketed sampling). Ketika polling atau WebSocket stream memicu perubahan state React sebelum proses mounting selesai, snapshot render SSR langsung terdistorsi.

Pola Kode yang Bermasalah (Error-Prone Pattern)

Berikut adalah contoh implementasi komponen galeri Next.js/React yang memicu hydration error karena membaca properti window dan memanipulasi dimensi di initial render:

// BadImageCard.jsx - Memicu: Error: Hydration failed because the initial UI does not match...
import { useState, useEffect } from 'react';

export function BadImageCard({ imageId, backendPayload }) {
  // Client menghitung dimensi via window atau metadata lokal saat inisialisasi state
  const isWide = typeof window !== 'undefined' && window.innerWidth > 768;
  const initialRatio = backendPayload?.meta?.ratio || (isWide ? '16/9' : '1/1');

  const [aspectRatio, setAspectRatio] = useState(initialRatio);

  useEffect(() => {
    // Race condition: Image() browser event listener menimpa state sebelum hydration stabil
    const img = new Image();
    img.src = `/api/diffusion/output/${imageId}`;
    img.onload = () => {
      setAspectRatio(`${img.naturalWidth}/${img.naturalHeight}`);
    };
  }, [imageId]);

  return (
    <div style={{ aspectRatio: aspectRatio }} className="gallery-item">
      {/* src blob atau path dinamis yang tidak sinkron antara server-client */}
      <img src={`/api/diffusion/output/${imageId}`} alt="AI Output" />
    </div>
  );
}

Solusi: Layout Reservation Berbasis CSS dan Mounting Deterministik

Solusi arsitektural yang benar tidak bergantung pada kalkulasi runtime JavaScript di client. Gunakan dua pendekatan berikut:

  1. Sertakan Metadata Dimensi dalam Payload SSR: Backend Python harus mengembalikan pasangan resolusi eksplisit (misalnya width: 1024, height: 768) bersama payload metadata awal, bukan dibaca asinkron via event onload browser.
  2. Reservasi Ruang dengan Pure CSS aspect-ratio: Terapkan rasio dimensi langsung pada container wrapper menggunakan CSS variabel atau inline style deterministik yang identik di server dan client.
  3. Isolasi Fitur Client-Only: Tunda pemuatan elemen yang benar-benar membutuhkan API browser lokal hingga hook useEffect tereksekusi penuh.

Implementasi Komponen yang Benar

// GoodImageCard.jsx - Bebas hydration error dan Cumulative Layout Shift (CLS)
'use client';

import { useState, useEffect } from 'react';
import Image from 'next/image';

export function GoodImageCard({ imageId, width, height, mimeType }) {
  const [isMounted, setIsMounted] = useState(false);

  // Isolasi eksekusi client-side murni
  useEffect(() => {
    setIsMounted(true);
  }, []);

  // Hitung aspect-ratio secara matematis tanpa API browser
  const calculatedRatio = width && height ? `${width} / ${height}` : '1 / 1';

  return (
    <div 
      className="relative w-full overflow-hidden bg-neutral-900 rounded-lg"
      style={{ aspectRatio: calculatedRatio }}
    >
      <img
        src={`/api/diffusion/output/${imageId}`}
        alt="Diffusion Output"
        width={width}
        height={height}
        loading="lazy"
        decoding="async"
        className="w-full h-full object-cover"
      />

      {/* Informasi client-only dirender HANYA setelah komponen termounting */}
      {isMounted && (
        <span className="absolute bottom-2 right-2 text-xs bg-black/60 px-2 py-1 rounded text-white">
          {width}x{height}
        </span>
      )}
    </div>
  );
}

Optimalisasi Kontrak Data API Backend

Pastikan endpoint Python/FastAPI mengembalikan dimensi native saat serialisasi response inferensi model:

# fastapi_app.py
from pydantic import BaseModel

class DiffusionResultResponse(BaseModel):
    image_id: str
    width: int
    height: int
    seed: int
    status: str

@app.get("/api/diffusion/task/{task_id}", response_model=DiffusionResultResponse)
async def get_diffusion_task(task_id: str):
    # Ambil metadata langsung dari layer inferensi tensor, bukan baca ulang disk
    task = await get_task_from_db(task_id)
    return {
        "image_id": task.id,
        "width": task.tensor_width,   # e.g., 832
        "height": task.tensor_height, # e.g., 1216 (ideogram portrait bucket)
        "seed": task.seed,
        "status": task.status
    }

Dengan mengunci metadata dimensi gambar dari server response dan memanfaatkan properti CSS aspect-ratio murni, DOM tree SSR dan hydration client akan tetap identik tanpa pergeseran layout (layout shift) maupun error mismatch konsol.