Akar Masalah: Precision Drift SQL vs JavaScript

Komputasi inferensi model AI sederhana (seperti logistic regression atau multi-layer perceptron) langsung di dalam database SQL adalah pola umum untuk memproses prediksi data dalam volume besar tanpa overhead network. Namun, saat skor probabilitas ini di-pass ke framework server-side rendering (SSR) seperti Next.js atau Remix, developer kerap mendapati error:

Error: Hydration failed because the initial UI does not match what was rendered on the server.

Penyebab utamanya adalah precision drift. PostgreSQL menyimpan floating-point 64-bit sebagai DOUBLE PRECISION (IEEE 754 binary64), sedangkan V8/JavaScript engine mengonversinya kembali melalui driver database (seperti pg). Selama proses transfer: komputasi database → serialisasi JSON driver → transfer SSR → re-evaluasi client-side, digit desimal paling belakang dapat mengalami pergeseran representasi biner.

Misalnya, kalkulasi fungsi aktivasi Sigmoid di SQL menghasilkan 0.8749201948192048. Di server, string HTML dirender dengan teks tersebut. Di browser client, representasi objek yang didehidrasi dari window.__NEXT_DATA__ atau proses formatting lokal dapat mengevaluasi nilai tersebut menjadi 0.874920194819205. Perbedaan satu karakter teks pada Virtual DOM node langsung memicu hydration mismatch fatal.

Skenario Kasus: Query Forward-Pass dan Komponen Bermasalah

Berikut adalah query SQL PostgreSQL yang mengeksekusi forward-pass fungsi aktivasi Sigmoid untuk menghitung probabilitas retensi pelanggan:

-- Query kalkulasi skor probabilitas retensi
SELECT
  id AS customer_id,
  -- Sigmoid: 1 / (1 + exp(-z))
  1.0 / (1.0 + EXP(-( -2.14 + (0.045 * session_count) + (0.78 * engagement_rate) ))) AS raw_score
FROM customer_features;

Database mengembalikan kolom raw_score bertipe DOUBLE PRECISION. Masalah muncul ketika komponen React merender nilai tersebut secara langsung atau menggunakan formatting dinamis tanpa spesifikasi locale yang kaku:

// ScoreBadge.buggy.tsx
// MASALAH: Render raw float dan toLocaleString() memicu hydration mismatch
interface Props {
  rawScore: number;
}

export function ScoreBadge({ rawScore }: Props) {
  return (
    <div className="score-card">
      <span>Probabilitas AI:</span>
      {/* 
        Bug 1: rawScore me-render perbedaan representasi float antara server string dan client DOM.
        Bug 2: toLocaleString() bergantung pada locale OS/browser client yang berbeda dengan server runtime.
      */}
      <strong>{rawScore * 100}%</strong>
    </div>
  );
}

Solusi 1: Normalisasi Deterministik di Boundary Data Access

Jangan pernah membiarkan floating-point mentah dari database masuk ke dalam render tree React. Normalisasikan nilai pada level Data Access Object (DAO) atau langsung di query SQL menggunakan fixed-point rounding.

Opsi A: Normalisasi di Query SQL (Paling Efisien)

Konversi tipe DOUBLE PRECISION menjadi NUMERIC dengan presisi tetap langsung pada database engine:

SELECT
  id AS customer_id,
  ROUND(
    CAST(1.0 / (1.0 + EXP(-( -2.14 + (0.045 * session_count) + (0.78 * engagement_rate) ))) AS NUMERIC), 
    4
  ) AS score
FROM customer_features;

Query di atas mengembalikan nilai deterministik (contoh: 0.8749) yang ditransmisikan sebagai string atau fixed numeric yang aman dari bit drift.

Opsi B: Normalisasi di Data Fetching Layer (Node.js)

Jika query SQL tidak dapat dimodifikasi, lakukan normalisasi sebelum data menyentuh komponen UI:

// data-access.ts
export interface PredictionRecord {
  customerId: string;
  normalizedScore: string; // Simpan representasi string deterministik
  rawScore: number;
}

export async function getPredictions(): Promise<PredictionRecord[]> {
  const rows = await db.query('SELECT id, raw_score FROM customer_features');
  
  return rows.map((row) => ({
    customerId: row.id,
    // Mengunci presisi ke 4 desimal secara deterministik
    normalizedScore: Number(row.raw_score).toFixed(4),
    rawScore: Number(row.raw_score),
  }));
}

Solusi 2: Implementasi Komponen SSR yang Aman

Komponen UI harus merender data berbasis teks tetap di server, dan menyimpan nilai numerik presisi tinggi pada atribut data-* jika dibutuhkan oleh library visualisasi (seperti Chart.js atau D3) di sisi client.

// ScoreBadge.fixed.tsx
interface SafeScoreBadgeProps {
  scoreText: string;   // Nilai string hasil .toFixed() atau SQL ROUND
  rawScore: number;    // Nilai numerik untuk konsumsi DOM attribute/client script
}

export function ScoreBadge({ scoreText, rawScore }: SafeScoreBadgeProps) {
  // Parsing eksplisit format persentase deterministik
  const percentage = (parseFloat(scoreText) * 100).toFixed(2);

  return (
    <div 
      className="score-card"
      data-raw-score={rawScore} // Aman: perbedaan float mikro pada attribute tidak memicu mismatch text content
    >
      <span>Probabilitas AI:</span>
      {/* Deterministik: Server dan client merender string yang identik */}
      <strong>{percentage}%</strong>
    </div>
  );
}

Aturan Formatting Dinamis

Jika aplikasi mewajibkan formatting spesifik locale pengguna (misalnya pemisah desimal koma , untuk id-ID dan titik . untuk en-US), jangan jalankan formatter langsung di SSR tree. Ikuti pola deferred hydration berikut:

  • SSR Baseline: Render format standar (ISO/EN) secara deterministik di server.
  • Client Progressive Enhancement: Jalankan Intl.NumberFormat di dalam hook useEffect setelah komponen terpasang di DOM.
  • Alternatif: Lewatkan locale pengguna dari HTTP Header Accept-Language server-side ke formatter, sehingga server dan client menggunakan locale input yang identik sejak awal.