Hydration error pada aplikasi berbasis SSR (seperti Next.js atau Nuxt) terjadi saat output HTML dari server tidak identik dengan DOM tree yang dihasilkan browser pada render pertama. Ketika menangani visualisasi data time-series bervolume tinggi, masalah ini sering dipicu oleh alur dekompresi data kompresi (misalnya encoding delta-of-delta dan XOR floating-point ala TimescaleDB) serta perbedaan penanganan zona waktu lokal.

Penyebab Utama Hydration Mismatch pada Data Time-Series

Saat mengirimkan data metrik terkompresi dari database seperti TimescaleDB ke layer frontend, ada dua titik kegagalan utama yang memicu perbedaan DOM antara Node.js runtime dan browser client:

  • Presisi Mikrodetik vs Milidetik: TimescaleDB menyimpan epoch dengan presisi mikrodetik (microseconds). JavaScript Date engine natively beroperasi pada resolusi milidetik (13-digit). Jika payload integer 16-digit langsung diparsing tanpa pemotongan digit presisi, runtime Node.js dan browser dapat menghasilkan teks label atau atribut SVG/Canvas (d, cx, x) yang bergeser.
  • Perbedaan Local Timezone SSR vs Browser: Node.js server biasanya berjalan dalam container dengan setting TZ=UTC. Browser pengguna berjalan sesuai sistem lokal (misal Asia/Jakarta atau UTC+7). Pemanggilan method format waktu lokal seperti toLocaleTimeString() atau Intl.DateTimeFormat menghasilkan string HTML berbeda di server vs client.
  • Inkonsistensi Float Bit-Level pada Dekompresi: Mengurai stream delta-of-delta dan XOR-decompression di dua environment JS yang berbeda dapat memicu variasi floating-point pada desimal terakhir, merusak kesesuaian koordinat path visualisasi SVG.

Analisis Kasus: Perbedaan Parse Waktu dan Float Desimal

Perhatikan contoh serialisasi metrik berikut. Pada server, representasi mikrodetik yang diformat langsung ke locale text menghasilkan DOM node yang berbeda:

// Data hasil dekompresi: [timestamp_micros, value]
const rawPoint = [1711929600123456, 42.10000000000001];

// Server (Node.js TZ=UTC):
// Render label: "00:00:00"

// Browser (Client TZ=Asia/Jakarta):
// Render label: "07:00:00"
// Hasil: Uncaught Error: Hydration failed because the initial UI does not match the server-rendered HTML.

Solusi 1: Normalisasi Epoch Timestamp dan Forced UTC

Lakukan normalisasi data mentah hasil dekompresi ke epoch milidetik standar dan kunci zona waktu render ke UTC atau berikan format seragam sebelum data disuntikkan ke komponen visualisasi.

// utils/timeseries.ts
export interface MetricPoint {
  timestamp: number; // Normalized to milliseconds
  value: number;
}

export function normalizeCompressedSeries(
  rawData: [number, number][]
): MetricPoint[] {
  return rawData.map(([micros, val]) => ({
    // Truncate microsecond to millisecond
    timestamp: Math.floor(micros / 1000),
    // Pangkas presisi float liar hasil unpacking XOR
    value: Number(val.toFixed(2)),
  }));
}

export function formatAxisTime(timestampMs: number): string {
  // Gunakan explicit timeZone agar server dan client konsisten
  return new Intl.DateTimeFormat('en-GB', {
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    timeZone: 'UTC',
  }).format(new Date(timestampMs));
}

Solusi 2: Pola Dynamic Import Tanpa SSR

Library visualisasi berbasis SVG/Canvas seperti Chart.js, ECharts, atau Apache ECharts memanipulasi DOM secara dinamis menggunakan kalkulasi getBoundingClientRect() yang tidak tersedia di Node.js. Menolak SSR untuk komponen chart merupakan pendekatan paling aman untuk pustaka grafik yang tidak deterministik.

// components/MetricsDashboard.tsx
import dynamic from 'next/dynamic';

// Matikan SSR secara eksplisit untuk komponen chart
const TimeSeriesChartClient = dynamic(
  () => import('./TimeSeriesChart'),
  {
    ssr: false,
    loading: () => <div className="chart-skeleton" style={{ height: 300 }} />,
  }
);

export default function MetricsDashboard({ rawPayload }: { rawPayload: [number, number][] }) {
  return (
    <div className="dashboard-container">
      <h3>Network Throughput</h3>
      <TimeSeriesChartClient rawData={rawPayload} />
    </div>
  );
}

Solusi 3: Isolasi Container dengan suppressHydrationWarning

Jika chart SVG statis tetap harus di-render di server untuk optimasi LCP (Largest Contentful Paint), gunakan atribut suppressHydrationWarning pada container SVG dan pisahkan logic render teks waktu ke client-only hook.

// components/StaticMetricNode.tsx
'use client';

import { useState, useEffect } from 'react';
import { formatAxisTime } from '../utils/timeseries';

export function MetricAxisLabel({ timestampMs }: { timestampMs: number }) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  return (
    <span suppressHydrationWarning>
      {mounted
        ? new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(new Date(timestampMs))
        : formatAxisTime(timestampMs)}
    </span>
  );
}

Trade-Off dan Rekomendasi

Pilihan perbaikan bergantung pada target arsitektur aplikasi:

  • Dynamic Import (ssr: false): Solusi paling stabil untuk chart interaktif berat. Menghilangkan hydration mismatch sepenuhnya, namun mengorbankan metrik initial paint visual grafik.
  • Normalisasi Data di Ingestion/API Layer: Paling efisien secara komputasi. Jangan lakukan dekompresi data mentah di browser jika payload delta-of-delta membebani thread JS client. Ubah data menjadi epoch milidetik seragam sebelum SSR page menerima props.