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.NumberFormatdi dalam hookuseEffectsetelah komponen terpasang di DOM. - Alternatif: Lewatkan locale pengguna dari HTTP Header
Accept-Languageserver-side ke formatter, sehingga server dan client menggunakan locale input yang identik sejak awal.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!