Hydration mismatch terjadi ketika representasi string dari Document Object Model (DOM) yang dihasilkan oleh Node.js pada tahap Server-Side Rendering (SSR) berbeda dengan DOM tree yang dibangun oleh browser pada kompilasi awal di client. Masalah ini kerap muncul pada visualisasi metrik Hyperparameter Optimization (HPO) saat membandingkan performa model LLM dengan algoritma klasik (misalnya Optuna, Random Forest, atau XGBoost).

Metrik HPO beroperasi pada domain angka pecahan kecil dengan presisi tinggi, seperti loss value (0.0001842) dan learning rate (1e-5 atau 0.00001). Inkonsistensi format angka antara runtime server dan browser langsung merusak proses hydration React, memicu re-render penuh, dan berisiko merusak visualisasi metrik.

Akar Masalah Hydration Mismatch Format Angka

Tiga faktor utama menyebabkan deviasi output numerik antara server dan browser:

  1. Locale Runtime Divergence: Node.js di server Docker/container sering kali berjalan dengan environment LANG=C.UTF-8 atau default en-US (menggunakan titik . sebagai pemisah desimal). Sebaliknya, browser client menggunakan locale lokal sistem operasi user (misalnya id-ID atau de-DE yang menggunakan koma ,). Pemanggilan num.toLocaleString() tanpa argumen locale eksplisit menghasilkan output berbeda.
  2. Batas Scientific Notation JavaScript: Mesin V8 secara otomatis mengubah float menjadi scientific notation jika nilai absolutnya berada di bawah 1e-6 ketika dikonversi via toString() atau string template literal. Evaluasi representasi string tanpa determinasi eksponen memicu diskrepansi representasi teks.
  3. Variasi Serialisasi Float IEEE-754: Floating-point 64-bit dapat menghasilkan ekor presisi non-deterministik saat ditransformasikan bolak-balik antara JSON payload API backend Python (seperti MLflow atau Ray Tune) dan JavaScript engine jika tidak dilakukan pembulatan deterministik sebelum render.

Contoh Kode yang Memicu Hydration Error

Komponen di bawah ini mereproduksi error secara konsisten ketika dibuka oleh browser dengan locale non-US (misalnya Indonesia atau Jerman):

// components/HpoRowBuggy.tsx
interface MetricProps {
  trialId: string;
  loss: number;        // Contoh: 0.00004218
  learningRate: number; // Contoh: 1e-4
}

export function HpoRowBuggy({ trialId, loss, learningRate }: MetricProps) {
  return (
    <tr>
      <td>{trialId}</td>
      {/* BUG: toLocaleString() bergantung pada locale sistem host */}
      <td>{loss.toLocaleString(undefined, { maximumFractionDigits: 6 })}</td>
      {/* BUG: String() implisit dapat berubah format tergantung engine/besaran */}
      <td>{learningRate}</td>
    </tr>
  );
}

Di server, output HTML berupa <td>0.000042</td>. Di client dengan locale Indonesia, browser menghasilkan <td>0,000042</td>. React menghentikan proses rekonsiliasi dan mengeluarkan pesan peringatan:

Error: Hydration failed because the server-rendered HTML didn't match the client.
See more info here: https://react.dev/link/hydration-mismatch
- <td>0.000042</td>
+ <td>0,000042</td>

Anti-Pattern: Menghindari Solusi Palsu

Dua pendekatan umum yang sering salah digunakan:

  • suppressHydrationWarning: Hanya membungkam log error di konsol, namun DOM node tetap tidak sinkron hingga terjadi interaksi berikutnya.
  • Conditional Client-Only Rendering (useEffect flag): Merender angka hanya setelah mounting client selesai. Pendekatan ini memicu Cumulative Layout Shift (CLS) dan merusak SEO pada tabel metrik benchmark.

Solusi Deterministik: Invariant Locale Boundary

Solusi yang benar adalah memaksakan invariant locale formatter pada level modul (shared antara SSR dan client) menggunakan standar Intl.NumberFormat dengan konfigurasi eksplisit.

// lib/hpoFormatters.ts

// Format invariant: Selalu gunakan locale teknis baku (en-US) untuk metrik komputasi
const lossFormatter = new Intl.NumberFormat('en-US', {
  minimumFractionDigits: 4,
  maximumFractionDigits: 6,
  useGrouping: false,
});

const lrFormatter = new Intl.NumberFormat('en-US', {
  notation: 'scientific',
  maximumSignificantDigits: 3,
});

export function formatLoss(value: number): string {
  if (!Number.isFinite(value)) return 'N/A';
  // ponytail: precision truncate beyond 6 digits, upgrade to BigNumber if sub-nano loss needed
  return lossFormatter.format(value);
}

export function formatLearningRate(value: number): string {
  if (!Number.isFinite(value)) return 'N/A';
  return lrFormatter.format(value).toLowerCase();
}

Implementasi komponen React yang aman dari hydration mismatch:

// components/HpoMetricRow.tsx
import { formatLoss, formatLearningRate } from '../lib/hpoFormatters';

interface HpoMetricRowProps {
  trialId: string;
  loss: number;
  learningRate: number;
}

export function HpoMetricRow({ trialId, loss, learningRate }: HpoMetricRowProps) {
  return (
    <tr>
      <td className="font-mono">{trialId}</td>
      <td className="font-mono text-right">{formatLoss(loss)}</td>
      <td className="font-mono text-right">{formatLearningRate(learningRate)}</td>
    </tr>
  );
}

Validasi Otomatis (Runnable Assertion)

Jalankan assertion script berikut di Node.js untuk membuktikan determinasi format output pada berbagai nilai uji metrik HPO:

// scripts/assert-formatters.mjs
import assert from 'node:assert/strict';

const lossFormatter = new Intl.NumberFormat('en-US', {
  minimumFractionDigits: 4,
  maximumFractionDigits: 6,
  useGrouping: false,
});

const lrFormatter = new Intl.NumberFormat('en-US', {
  notation: 'scientific',
  maximumSignificantDigits: 3,
});

function formatLoss(val) {
  return Number.isFinite(val) ? lossFormatter.format(val) : 'N/A';
}

function formatLR(val) {
  return Number.isFinite(val) ? lrFormatter.format(val).toLowerCase() : 'N/A';
}

// Uji 1: Precision loss rounding
assert.equal(formatLoss(0.000042188), '0.000042');
assert.equal(formatLoss(0.12), '0.1200');

// Uji 2: Scientific notation deterministik untuk learning rate
assert.equal(formatLR(0.00001), '1e-5');
assert.equal(formatLR(0.00025), '2.5e-4');

// Uji 3: Edge cases numerik ML
assert.equal(formatLoss(NaN), 'N/A');
assert.equal(formatLoss(Infinity), 'N/A');

console.log('Semua format invariant valid.');

skipped: caching locale dinamis per user, add when dashboard butuh switch locale tampilan angka di luar metrik teknis.