Hydration drift terjadi ketika HTML hasil render di server tidak sama dengan hasil render awal di browser. Pada dashboard energi atau jaringan, masalah ini sering muncul karena data time series, metrik yang terus berubah, pemformatan angka lintas locale, komponen peta, dan chart yang bergantung pada API browser. Akibatnya, framework SSR akan memberi peringatan mismatch, membuang DOM yang sudah ada, atau melakukan re-render penuh di client.

Untuk SSR stabil untuk dashboard energi, prinsip utamanya sederhana: render awal harus deterministik. Artinya, server dan client harus membaca snapshot data yang sama, memakai format yang sama, dan menunda komponen yang tidak aman di SSR sampai browser siap. Ini penting untuk aplikasi perencanaan jaringan, studi kapasitas, atau eksplorasi skenario seperti use case open source perencanaan energi skala besar: dataset besar, peta interaktif, kurva beban, aliran lintas wilayah, dan KPI yang sering diperbarui.

Mengapa hydration drift sering terjadi pada dashboard energi

Dashboard energi punya beberapa karakteristik yang membuat mismatch lebih mudah terjadi dibanding halaman konten biasa:

  • Data berubah cepat: harga, beban, dispatch, atau status jaringan bisa berubah di sela waktu antara SSR dan hydration.
  • Banyak angka turunan: total, rata-rata, persentase, pembulatan unit, dan agregasi sering dihitung dua kali di tempat berbeda.
  • Time series sensitif zona waktu: label waktu dapat berbeda antara server dan browser.
  • Komponen visual berat: chart, map, dan layer geospasial sering mengakses window, document, ukuran viewport, atau WebGL.
  • State UI kompleks: filter, skenario, area peta, dan tab aktif bisa memengaruhi struktur DOM awal.

Pada praktiknya, masalah bukan hanya peringatan di console. Hydration drift dapat menyebabkan:

  • flash konten berbeda saat halaman dimuat,
  • event handler menempel ke elemen yang salah,
  • re-render penuh yang mahal untuk tabel besar atau grafik kompleks,
  • hasil analisis yang membingungkan karena angka berubah sesaat setelah halaman tampil.

Penyebab mismatch yang paling umum

1. Timestamp dan zona waktu

Ini penyebab paling sering. Server bisa berjalan di UTC, sementara browser pengguna memakai zona waktu lokal. Jika Anda merender label seperti 08:00 di server dan client mengubahnya menjadi 15:00, mismatch terjadi.

Masalah umum:

  • Memanggil new Date() langsung saat render.
  • Memformat tanggal tanpa menetapkan timezone dan locale secara eksplisit.
  • Menggunakan waktu “sekarang” saat SSR lalu menghitung ulang di client beberapa milidetik kemudian.

Pola aman: kirim timestamp mentah dari server, lalu gunakan formatter yang konsisten. Jika output harus identik saat hydration, render dulu format netral yang stabil, atau sertakan timezone yang dipakai server di payload state.

// shared/formatters.ts
export function formatUtcHour(isoString: string) {
  return new Intl.DateTimeFormat('id-ID', {
    hour: '2-digit',
    minute: '2-digit',
    timeZone: 'UTC'
  }).format(new Date(isoString));
}

Jika kebutuhan produk memang menuntut tampilan waktu lokal pengguna, pertimbangkan render awal yang netral seperti ISO ringkas atau placeholder stabil, lalu ubah setelah komponen client aktif. Jangan mengharapkan hasil SSR dan client identik jika timezone berbeda tetapi format lokal langsung dirender di kedua sisi.

2. Locale dan pemformatan angka

Dashboard energi sering menampilkan MW, MWh, GW, persen, dan nilai mata uang. Pemformatan angka yang tidak konsisten dapat membuat HTML berbeda, misalnya pemisah ribuan, digit desimal, atau notasi kompak.

Contoh sumber masalah:

  • Server memakai locale default lingkungan runtime.
  • Browser pengguna memakai locale berbeda dari server.
  • Format desimal ditentukan implisit, bukan eksplisit.

Pola aman:

  • Tentukan locale dan opsi formatter secara eksplisit.
  • Jika angka adalah hasil perhitungan, kirim nilai final yang sudah dibulatkan dari server bila konsistensi tampilan lebih penting daripada fleksibilitas.
  • Hindari membulatkan di server dengan satu aturan lalu menghitung ulang di client dengan aturan lain.
export function formatMw(value: number) {
  return new Intl.NumberFormat('id-ID', {
    minimumFractionDigits: 1,
    maximumFractionDigits: 1
  }).format(value) + ' MW';
}

3. Random ID atau nilai non-deterministik

Komponen tabel, accordion, tooltip, atau SVG chart kadang membuat ID acak untuk aria-describedby, gradient, clipPath, atau relasi label-elemen. Jika server menghasilkan ID berbeda dari client, node bisa dianggap tidak cocok.

Hindari:

const id = Math.random().toString(36).slice(2);

Pola aman:

  • Bangun ID dari data yang stabil, misalnya regionCode, seriesKey, atau assetId.
  • Jika framework menyediakan mekanisme ID yang aman untuk SSR, gunakan sesuai pola resminya.
  • Jangan membuat ID acak saat render awal.
const id = `series-${regionCode}-${metricKey}`;

4. Pembulatan angka dan perhitungan turunan

Agregasi energi sering melibatkan angka pecahan, unit conversion, dan pembulatan presentasi. Drift muncul saat server dan client menghitung ulang dengan urutan operasi berbeda.

Contoh: server menjumlahkan semua nilai mentah lalu membulatkan hasil akhir, sementara client membulatkan tiap titik dulu lalu menjumlahkan. Keduanya bisa berbeda.

Pola aman:

  • Pilih satu sumber kebenaran untuk kalkulasi presentasi.
  • Untuk angka yang tampil di atas lipatan pertama, lebih aman kirim hasil final yang siap render.
  • Jika perhitungan harus dilakukan di client juga, gunakan fungsi utilitas bersama yang benar-benar sama.
// shared/energy.ts
export function toDisplayLoad(values: number[]) {
  const total = values.reduce((sum, n) => sum + n, 0);
  return Number(total.toFixed(1));
}

5. Data fetch ganda dengan hasil berbeda

Masalah klasik di SSR: server mengambil data A saat render HTML, lalu client mengambil ulang data yang sudah berubah beberapa detik kemudian sebelum hydration selesai atau segera setelahnya. Hasilnya, UI awal tidak identik.

Skenario umum:

  • SSR fetch ke endpoint /metrics/latest, lalu komponen client melakukan fetch ulang di onMounted atau useEffect.
  • Server memakai cache snapshot, client langsung ke API real-time.
  • Urutan resolusi promise berbeda sehingga DOM awal berubah.

Pola aman:

  • Gunakan server snapshot sebagai sumber state hydration.
  • Serialisasikan data SSR secara eksplisit ke payload halaman.
  • Lakukan refresh client setelah hydration selesai, bukan selama render awal.

6. State turunan yang dihitung di tempat berbeda

Misalnya, server merender tab default “Transmission”, tetapi di client tab aktif dipilih ulang berdasarkan lebar layar, query param yang terlambat terbaca, atau state store yang menginisialisasi nilai lain. Mismatch DOM pun terjadi.

Pola aman:

  • Turunkan state awal dari payload SSR yang sama.
  • Jangan mengubah struktur DOM selama render awal berdasarkan kondisi yang hanya ada di browser.
  • Jika perlu adaptasi berbasis viewport, lakukan setelah mount dengan fallback yang stabil.

7. Chart dan map yang hanya aman di client

Banyak pustaka chart dan peta mengandalkan ukuran elemen, canvas, SVG dinamis, WebGL, atau API browser. Pada dashboard jaringan, peta interkoneksi, heatmap node, dan kurva beban interaktif sering tidak cocok dirender penuh di server.

Tanda-tandanya:

  • Error window is not defined atau document is not defined.
  • Mismatch pada jumlah node SVG/canvas wrapper.
  • Layout berubah setelah browser mengetahui ukuran kontainer.

Pola aman: jadikan komponen ini client-only dan render placeholder SSR yang stabil.

Pola implementasi yang aman untuk SSR stabil

1. Gunakan server snapshot sebagai kontrak render awal

Untuk halaman dashboard energi, anggap render awal sebagai foto statis dari satu momen data. Jangan menggabungkan sebagian data dari server dengan sebagian lagi dari fetch client sebelum hydration selesai.

Prinsipnya:

  1. Server mengambil semua data minimum yang dibutuhkan untuk tampilan awal.
  2. Server menghitung nilai turunan yang perlu tampil konsisten.
  3. Payload diserialisasikan ke halaman.
  4. Client menginisialisasi store dari payload yang sama.
  5. Refresh data live dilakukan setelah mount atau lewat interaksi pengguna.
// Contoh bentuk payload snapshot
{
  "generatedAt": "2026-01-10T12:00:00Z",
  "timezone": "UTC",
  "filters": {
    "scenario": "base",
    "region": "eu-central"
  },
  "summary": {
    "loadMw": 18234.1,
    "renewablesShare": 47.3
  },
  "series": [
    { "timestamp": "2026-01-10T11:00:00Z", "loadMw": 18010.2 }
  ]
}

Dengan model ini, semua komponen SSR membaca data yang sama. Jika ada polling atau sinkronisasi real-time, jalankan setelah UI selesai terhidrasi.

2. Serialisasi state secara eksplisit, jangan berharap fetch ulang hasilnya sama

Masalah hydration sering disamarkan oleh asumsi bahwa endpoint yang sama akan mengembalikan respons yang sama beberapa saat kemudian. Pada sistem energi, asumsi ini lemah.

Praktik yang aman:

  • Simpan payload snapshot apa adanya, termasuk filter aktif, timezone, versi skenario, dan cap waktu pembangkitan data.
  • Jika ada data turunan penting, kirim juga hasil turunannya agar client tidak menghitung ulang saat render awal.
  • Pastikan struktur serialisasi stabil: nama field konsisten, nilai kosong eksplisit, urutan transformasi jelas.

Ini juga mempermudah debugging, karena Anda bisa membandingkan payload SSR dengan state store client saat hydration.

3. Gate komponen client-only dengan fallback stabil

Chart besar, peta, dan panel yang bergantung pada viewport sebaiknya tidak dipaksa SSR penuh jika hasilnya tidak deterministik. Solusinya bukan menonaktifkan SSR untuk seluruh halaman, tetapi hanya untuk komponen yang berisiko.

Fallback yang baik:

  • tinggi kontainer tetap,
  • skeleton atau placeholder dengan struktur stabil,
  • angka ringkasan tetap ditampilkan dari SSR,
  • teks status seperti “Memuat visualisasi…” tanpa mengubah layout drastis.
<section class="chart-panel">
  <h2>Beban Sistem 24 Jam</h2>
  <div class="chart-shell" style="min-height:320px">
    <!-- SSR fallback stabil -->
    <p>Memuat visualisasi...</p>
  </div>
</section>

Setelah komponen client aktif, gantikan isi shell tersebut dengan chart yang sesungguhnya. Dengan begitu, SSR tetap memberi konten cepat dan stabil, sementara visual interaktif diinisialisasi aman di browser.

4. Gunakan formatter bersama untuk tanggal dan angka

Jangan menaruh logika format tersebar di banyak komponen. Simpan dalam modul bersama agar server dan client menjalankan aturan yang sama.

  • Satu modul untuk tanggal/waktu.
  • Satu modul untuk satuan energi dan daya.
  • Satu modul untuk pembulatan dan konversi unit.

Ini mengurangi mismatch dan mencegah perbedaan kecil yang sulit dilacak.

5. Pisahkan state sumber dan state presentasi

State sumber adalah payload snapshot dari server. State presentasi adalah hasil interaksi browser: zoom peta, baris tabel yang dipilih, tooltip aktif, atau preferensi panel. Jangan campur keduanya saat render awal.

Pola yang sehat:

  • source state: hasil SSR, dipakai untuk markup awal.
  • client UI state: diinisialisasi setelah mount, tidak mengubah struktur kritis sebelum hydration selesai.

Contoh alur implementasi lintas framework

Next.js

Pada Next.js, prinsipnya tetap: hasil data untuk SSR harus menjadi state awal client. Hindari memanggil endpoint yang sama lagi saat komponen pertama kali dirender jika tujuannya hanya mengisi ulang state yang sudah ada.

Praktik yang disarankan:

  • Ambil data di sisi server untuk route halaman.
  • Serialisasikan snapshot ke props atau mekanisme data route yang sesuai.
  • Inisialisasi store client dari snapshot itu.
  • Untuk chart/map yang tidak aman di SSR, gunakan pemuatan client-only dengan fallback yang stabil.
// Pseudocode pola umum
export default function EnergyDashboard({ snapshot }) {
  const [state] = useState(() => snapshot);

  useEffect(() => {
    // refresh opsional setelah hydration
    // fetchLatest().then(updateState)
  }, []);

  return <DashboardView initialState={state} />;
}

Hal yang perlu dihindari: membaca window.innerWidth, navigator.language, atau new Date() langsung di jalur render yang memengaruhi markup SSR.

Nuxt

Di Nuxt, masalah umum muncul saat data diambil ulang pada lifecycle client tanpa mempertahankan snapshot yang sama. Pastikan data awal halaman berasal dari payload SSR, bukan state kosong yang segera diisi ulang di browser.

Pola aman:

  • Gunakan mekanisme data async server untuk snapshot awal.
  • Hindari transformasi angka/tanggal yang berbeda antara server dan client.
  • Gunakan pembungkus client-only untuk komponen peta dan chart yang tidak SSR-safe.

Nuxt juga perlu perhatian pada plugin yang hanya cocok di browser. Jika plugin chart atau map menyentuh DOM saat import, muat hanya di client.

SvelteKit

Di SvelteKit, bug hydration sering muncul dari kode yang bercampur antara load server dan inisialisasi browser di komponen. Data hasil load harus menjadi satu-satunya sumber render awal.

Pola aman:

  • Hitung snapshot di load.
  • Jangan menghitung ulang nilai yang sama di komponen saat pertama render.
  • Letakkan akses browser-only pada blok yang hanya berjalan di client.
// Pseudocode ide umum
export async function load() {
  const snapshot = await getDashboardSnapshot();
  return { snapshot };
}

Untuk visualisasi yang berat, tampilkan shell SSR lebih dulu lalu pasang komponen interaktif setelah browser siap.

Strategi fallback yang benar-benar stabil

Fallback bukan sekadar skeleton acak. Pada dashboard energi, fallback harus menjaga ukuran layout dan konteks analitis agar pengguna tidak merasa data “berubah sendiri”.

Fallback yang baik biasanya memiliki:

  • judul panel yang sama dengan komponen final,
  • angka ringkasan SSR yang tetap,
  • dimensi kontainer final agar tidak terjadi layout shift besar,
  • teks status yang tidak bergantung pada waktu lokal atau data acak.

Contoh: pada panel peta jaringan, render dulu ringkasan SSR seperti jumlah koridor, kapasitas total, dan wilayah terpilih. Peta interaktifnya sendiri baru diaktifkan di client. Pengguna tetap mendapat informasi penting tanpa memicu hydration drift.

Kesalahan umum yang sering lolos review

  • Memakai index array sebagai key pada daftar yang bisa berubah urutan setelah fetch ulang.
  • Mengurutkan data di client saja padahal server merender urutan berbeda.
  • Menghapus nilai null/undefined secara berbeda antara server dan client.
  • Mengandalkan locale default environment untuk angka dan tanggal.
  • Membaca preferensi pengguna dari localStorage saat render awal lalu mengubah struktur DOM.
  • Menghitung “updated x seconds ago” di SSR yang langsung basi saat hydration.
  • Mengimpor library browser-only di level modul sehingga evaluasi sudah gagal atau hasil render berbeda.

Checklist debugging hydration drift

Jika tim Anda melihat warning hydration pada dashboard energi, pakai urutan pemeriksaan berikut sebelum menyalahkan framework:

  1. Bandingkan payload SSR dan state client awal.
    Pastikan nilai snapshot identik sebelum refresh data berjalan.
  2. Cari semua penggunaan waktu saat render.
    Audit new Date(), formatter tanggal, label “sekarang”, dan durasi relatif.
  3. Audit formatter angka dan locale.
    Pastikan locale, digit desimal, dan unit sama di server dan client.
  4. Cari sumber nilai acak.
    Periksa Math.random(), UUID saat render, atau ID generated dari library.
  5. Matikan refresh client sementara.
    Jika mismatch hilang, kemungkinan masalah berasal dari fetch ganda atau polling terlalu cepat.
  6. Stub komponen chart/map.
    Ganti dengan placeholder statis. Jika warning hilang, berarti komponen visual tidak SSR-safe.
  7. Periksa state turunan.
    Urutan sort, filter default, tab aktif, dan agregasi harus identik.
  8. Bandingkan HTML server dengan DOM sebelum interaksi.
    Gunakan view-source, panel network, atau snapshot testing untuk melihat perbedaan markup awal.

Pertanyaan diagnostik yang berguna

  • Apakah server dan client memformat tanggal dengan timezone yang sama?
  • Apakah angka dirender dari nilai mentah yang sama?
  • Apakah ada komponen yang bergantung pada ukuran layar saat render?
  • Apakah fetch client berjalan sebelum hydration selesai?
  • Apakah ada plugin chart/map yang mengakses DOM saat import?

Kapan SSR penuh tidak perlu dipaksakan

Tidak semua panel pada dashboard energi harus SSR penuh. Jika sebuah visualisasi sangat bergantung pada browser dan tidak memberi nilai SEO atau konten awal yang penting, lebih aman menjadikannya client-only. Yang tetap perlu SSR biasanya adalah:

  • judul halaman dan struktur navigasi,
  • filter aktif awal,
  • angka ringkasan utama,
  • tabel ringkas atau narasi status sistem,
  • shell layout untuk panel interaktif.

Pendekatan ini memberi keseimbangan: halaman tetap cepat, bermakna, dan dapat dipantau, tanpa memaksa semua visualisasi berat ikut SSR.

Ringkasan implementasi yang bisa langsung dipakai tim frontend

  • Buat snapshot server sebagai satu-satunya sumber render awal.
  • Serialisasikan state secara eksplisit, termasuk filter, timestamp, timezone, dan nilai turunan penting.
  • Pakai formatter bersama untuk angka dan tanggal.
  • Hindari nilai non-deterministik saat render: waktu saat ini, random ID, locale default, dan pembulatan yang tidak konsisten.
  • Jadikan chart/map client-only bila tidak SSR-safe, dengan fallback stabil.
  • Tunda refresh data live sampai hydration selesai.
  • Pisahkan source state dari client UI state.
  • Sediakan checklist debugging yang dipakai bersama lintas Next.js, Nuxt, dan SvelteKit.

Pada dashboard energi dan jaringan, hydration drift bukan masalah kosmetik. Ia biasanya menandakan bahwa kontrak data dan render awal belum deterministik. Begitu tim menyamakan snapshot SSR, serialisasi state, dan batas komponen client-only, warning mismatch biasanya turun drastis, render awal menjadi stabil, dan dashboard lebih mudah di-debug serta dipelihara.