Hydration error terjadi pada framework SSR (Server-Side Rendering) seperti Next.js atau Nuxt ketika tree DOM yang dihasilkan oleh server tidak cocok dengan representasi Virtual DOM pada render awal di browser. Masalah ini sering muncul saat mengintegrasikan elemen grafis Three.js, terutama aset prosedural seperti hasil komputasi Three.js-Object-Sculptor.

Akar Masalah Hydration Error pada Three.js

Server Node.js tidak memiliki Web API seperti objek window, document, maupun konteks WebGL (HTMLCanvasElement.getContext('webgl')). Ada dua faktor utama pemicu hydration mismatch:

  • Akses Langsung ke Web API: Menjalankan inisialisasi new THREE.WebGLRenderer() di luar lifecycle client-side memicu eksepsi ReferenceError: window is not defined di lingkungan server.
  • DOM Mutation Mismatch: Inisialisasi Three.js biasanya menyisipkan elemen <canvas> baru ke dalam container DOM melalui containerRef.appendChild(renderer.domElement). Jika server menghasilkan <div></div> kosong tetapi browser langsung menyisipkan tag <canvas> sebelum hidrasi React/Vue selesai, parser hidrasi mendeteksi perbedaan node anak dan menghasilkan error.
  • Komputasi Non-deterministik: Generator geometri prosedural sering mengandalkan Math.random() atau parameter floating-point dinamis. Perbedaan nilai antara server dan client akan merusak kecocokan atribut DOM.

Pola Solusi: Client-Only Dynamic Import & Lifecycle Mounting

Pendekatan terbersih untuk mematikan rendering server pada elemen WebGL adalah kombinasi modul dinamis (dynamic import tanpa SSR) dan isolasi lifecycle. Ini memastikan Three.js hanya dievaluasi setelah runtime browser aktif sepenuhnya.

1. Komponen Kanvas Three.js Prosedural

Pisahkan implementasi Three.js ke dalam berkas komponen terisolasi, misalnya ProceduralSculptorCanvas.jsx. Pastikan cleanup listener dan WebGL context disposal ditangani saat unmount.

'use client';

import { useEffect, useRef } from 'react';
import * as THREE from 'three';

export default function ProceduralSculptorCanvas() {
  const mountRef = useRef(null);

  useEffect(() => {
    const currentMount = mountRef.current;
    if (!currentMount) return;

    // Inisialisasi Scene, Kamera, Renderer
    const width = currentMount.clientWidth;
    const height = currentMount.clientHeight;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
    camera.position.z = 3;

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setSize(width, height);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    currentMount.appendChild(renderer.domElement);

    // Geometri prosedural sederhana (contoh skenario Three.js-Object-Sculptor)
    const geometry = new THREE.TorusKnotGeometry(0.8, 0.25, 100, 16);
    const material = new THREE.MeshNormalMaterial({ wireframe: true });
    const mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);

    let animationFrameId;
    const animate = () => {
      mesh.rotation.x += 0.01;
      mesh.rotation.y += 0.015;
      renderer.render(scene, camera);
      animationFrameId = requestAnimationFrame(animate);
    };
    animate();

    // Cleanup resources
    return () => {
      cancelAnimationFrame(animationFrameId);
      renderer.dispose();
      geometry.dispose();
      material.dispose();
      if (currentMount.contains(renderer.domElement)) {
        currentMount.removeChild(renderer.domElement);
      }
    };
  }, []);

  return <div ref={mountRef} className="sculptor-viewport" style={{ width: '100%', height: '400px' }} />;
}

2. Wrapper Client-Side dengan Skeleton Fallback

Gunakan dynamic import dengan opsi ssr: false pada Next.js (atau <ClientOnly> pada Nuxt). Sediakan skeleton loader dengan dimensi CSS yang identik agar tidak terjadi Cumulative Layout Shift (CLS).

import dynamic from 'next/dynamic';

const ProceduralSculptorCanvas = dynamic(
  () => import('./ProceduralSculptorCanvas'),
  {
    ssr: false,
    loading: () => (
      <div 
        style={{ width: '100%', height: '400px', backgroundColor: '#111' }} 
        aria-label="Memuat kanvas 3D..."
      />
    ),
  }
);

export default function Page() {
  return (
    <main>
      <h2>Visualisasi Prosedural 3D</h2>
      <ProceduralSculptorCanvas />
    </main>
  );
}

Debugging Checklist

  1. Periksa Tree Skeleton: Pastikan tag HTML loader di sisi server menghasilkan elemen dengan tipe dan atribut inline yang stabil.
  2. Cek Mutasi Langsung: Jangan pernah memanipulasi atribut kanvas lewat selektor global seperti document.querySelector('canvas') di luar hook lifecycle.
  3. Disposal Context: Pastikan renderer.dispose() dipanggil saat unmount untuk mencegah memory leak WebGL context limit error pada navigasi SPA.