SSR deterministik untuk UI AI berarti HTML yang dirender di server harus cocok dengan render awal di browser. Pada antarmuka yang menampilkan hasil AI atau komputasi yang berubah-ubah, mismatch saat hydration sering terjadi karena nilai seperti ringkasan, confidence, formatting angka, timestamp, atau urutan token berbeda antara server dan client.

Masalah ini bukan sekadar warning kosmetik. Hydration mismatch bisa menyebabkan DOM di-recreate, state event handler tidak konsisten, layout bergeser, dan perilaku UI sulit ditebak. Jika konteksnya adalah fitur seperti “AI does the math”, risikonya makin tinggi karena hasil komputasi, pembulatan, atau narasi penjelasan bisa berubah tergantung lingkungan render. Solusinya bukan “jangan pakai SSR”, tetapi memastikan output awal benar-benar deterministik.

Mengapa UI AI rawan mismatch saat hydration

Pada SSR, server mengirim HTML awal. Setelah JavaScript client dimuat, framework seperti React atau Vue melakukan hydration: mengaitkan komponen interaktif ke HTML yang sudah ada. Jika render pertama di client menghasilkan markup berbeda, framework akan mendeteksi mismatch.

UI AI lebih rawan daripada UI CRUD biasa karena banyak data atau turunan state yang tidak stabil:

  • Hasil komputasi berubah: confidence, ranking, skor, summary, atau explanation dapat berbeda jika dihitung ulang.
  • Formatter berbeda: locale, timezone, pembulatan angka, dan format tanggal dapat berubah antara server dan browser.
  • Streaming parsial: server mungkin mengirim hasil sementara, tetapi client langsung merender hasil final.
  • Randomness tersembunyi: seed acak, urutan object, atau ID unik dari library dapat memengaruhi output.
  • Derived state di client: komponen menghitung ulang label, badge, atau klasifikasi dari data mentah, lalu hasilnya tidak sama dengan yang ada di HTML server.

Prinsip utama: jangan hitung ulang hal yang memengaruhi markup awal di client jika server sudah menghitungnya. Kirim hasil final yang akan ditampilkan sebagai bagian dari state awal, lalu hydrate menggunakan state yang sama.

Penyebab umum mismatch pada SSR deterministik untuk UI AI

1. Timestamp, timezone, dan locale

Kasus paling umum adalah server merender 12/08/2026, 09:00, tetapi browser merender format berbeda karena locale pengguna, timezone lokal, atau API formatter yang berbeda.

Contoh masalah:

// rentan mismatch jika timezone/locale berbeda
const label = new Date(result.generatedAt).toLocaleString();

Jika label itu muncul saat SSR dan dihitung ulang di client, markup awal bisa berbeda.

Pendekatan aman:

  • Serialisasikan string final dari server jika perlu tampil saat SSR.
  • Atau render placeholder stabil saat SSR, lalu format ulang setelah mounted di client.
  • Gunakan timezone dan locale eksplisit bila formatting harus dilakukan di dua sisi.

2. Random seed dan ID yang tidak stabil

Jangan gunakan Math.random(), Date.now(), atau generator ID non-deterministik untuk nilai yang memengaruhi elemen SSR seperti key, ID atribut, urutan daftar, atau teks badge.

Masalah khas:

  • ID elemen berbeda antara server dan client.
  • Urutan rekomendasi berubah karena sorting berbasis skor yang dihitung ulang dengan seed berbeda.
  • Komponen menampilkan confidence atau sampling result yang tidak identik.

Pendekatan aman: hasilkan seed atau ID di server, simpan pada payload awal, dan gunakan ulang di client tanpa regenerasi.

3. State turunan dihitung ulang di client

Sering kali API mengirim data mentah, lalu UI menghitung:

  • label confidence: tinggi, sedang, rendah
  • ringkasan singkat dari output panjang
  • highlight bagian penting
  • format skor persen

Kalau rumus, pembulatan, atau dependency formatter berbeda sedikit saja, hasilnya tidak sama.

Pendekatan aman: untuk elemen yang harus muncul pada SSR, kirim juga view model yang sudah siap render, bukan hanya data mentah.

4. Streaming parsial dan output yang belum final

Pada UI AI, server sering mulai mengirim halaman sebelum hasil AI final tersedia. Ini baik untuk respons cepat, tetapi berbahaya jika server merender sebagian token atau confidence sementara, lalu client saat hydration langsung punya hasil final yang berbeda.

Pendekatan aman:

  • SSR hanya merender placeholder stabil untuk area AI yang belum final.
  • Hydration pertama menggunakan placeholder yang sama.
  • Setelah mounted atau setelah data final siap, barulah area itu diperbarui secara client-side.

5. Perbedaan formatter angka dan markdown

AI output sering berisi angka, tabel, markdown, atau ekspresi matematika. Perbedaan kecil pada parser markdown, whitespace, pembulatan, atau sanitizer HTML dapat memicu mismatch.

Contoh risiko:

  • Server membulatkan ke 2 digit, client ke 3 digit.
  • Server menghasilkan HTML markdown yang sudah disanitasi, client melakukan parsing ulang dengan aturan lain.
  • Ekspresi matematika dirender berbeda karena library client memproses ulang input mentah.

Pendekatan aman: render output presentasional satu kali di server dan kirim hasilnya sebagai payload final, atau tunda render kaya-format sampai client siap dengan boundary terpisah.

Pola implementasi yang paling aman

1. Bedakan data mentah, view model, dan state interaktif

Jangan campurkan semuanya. Struktur yang lebih aman:

  • Data mentah: respons model atau hasil komputasi asli.
  • View model deterministik: string, angka, label, HTML sanitasi, dan urutan daftar yang siap dirender.
  • State interaktif client: expand/collapse, copy state, tabs, atau mode tampilan.

SSR sebaiknya bergantung pada view model deterministik, bukan data mentah yang akan dihitung ulang di browser.

2. Serialisasi state awal secara eksplisit

Tujuannya adalah memastikan render awal di client membaca payload yang sama persis dengan yang dipakai server. Jangan melakukan fetch kedua pada hydration untuk data yang sama jika hasilnya bisa berubah.

Contoh shape payload yang aman:

{
  "resultId": "calc_123",
  "generatedAtIso": "2026-08-11T08:30:00.000Z",
  "view": {
    "summary": "Model memperkirakan nilai x = 42.",
    "confidenceLabel": "Sedang",
    "confidenceValue": 0.78,
    "formattedScore": "78%",
    "explanationHtml": "<p>Langkah utama...</p>"
  },
  "meta": {
    "locale": "id-ID",
    "timeZone": "UTC",
    "seed": "fixed-seed-123"
  }
}

Client memakai nilai ini apa adanya untuk render pertama. Jika nanti ingin mem-format ulang sesuai preferensi pengguna, lakukan setelah mounted dan jangan mengubah markup SSR sebelum hydration selesai.

3. Gunakan placeholder stabil untuk area non-deterministik

Jika hasil AI belum final, lebih baik render placeholder yang konsisten daripada mencoba merender konten parsial yang berubah cepat.

Placeholder yang baik:

  • tingginya stabil agar mencegah layout shift besar
  • teksnya tidak bergantung pada waktu atau progres dinamis saat SSR
  • tidak mengandung angka yang akan berubah saat hydration

Contoh:

<section aria-busy="true" aria-live="polite">
  <h2>Ringkasan AI</h2>
  <p>Hasil sedang disiapkan...</p>
</section>

Hindari placeholder seperti Diproses 37% jika nilai itu bisa berbeda antara server dan client.

4. Pakai client-only boundary untuk widget yang memang tidak cocok di-SSR

Tidak semua bagian harus SSR penuh. Untuk widget seperti streaming token, visualisasi confidence real-time, atau formatter matematis yang berat di client, boundary client-only sering lebih aman.

Trade-off-nya:

  • Keuntungan: mismatch jauh berkurang, implementasi lebih sederhana.
  • Kerugian: SEO untuk area tersebut hilang atau berkurang, initial content bisa kosong/placeholder, Time to Interactive bergantung pada JS.

Gunakan boundary ini hanya pada bagian yang benar-benar non-deterministik, bukan seluruh halaman jika konten utama tetap penting untuk crawl dan first paint.

Contoh implementasi di Next.js

SSR dengan view model yang diserialisasikan

Prinsipnya sama baik menggunakan App Router maupun Pages Router: hasilkan payload deterministik di server, lalu render komponen dari payload itu tanpa menghitung ulang bagian presentasional.

// contoh komponen server/client yang membaca payload deterministik
export default async function Page() {
  const result = await getAiMathResult();

  const initialState = {
    id: result.id,
    generatedAtIso: result.generatedAtIso,
    view: {
      summary: result.summaryText,
      confidenceLabel: result.confidenceLabel,
      formattedScore: result.formattedScore,
      explanationHtml: result.explanationHtml
    }
  };

  return (
    <main>
      <h1>Hasil Analisis</h1>
      <AiResult initialState={initialState} />
    </main>
  );
}
'use client';

import { useState } from 'react';

export function AiResult({ initialState }) {
  const [state] = useState(initialState);

  return (
    <section>
      <p><strong>Ringkasan:</strong> {state.view.summary}</p>
      <p><strong>Confidence:</strong> {state.view.confidenceLabel} ({state.view.formattedScore})</p>
      <div dangerouslySetInnerHTML={{ __html: state.view.explanationHtml }} />
    </section>
  );
}

Poin pentingnya bukan pada API tertentu, melainkan:

  • initialState sudah siap render.
  • Tidak ada pemanggilan formatter ulang saat render pertama di client.
  • useState(initialState) menjaga nilai awal tetap sama pada hydration.

Menunda formatting yang bergantung browser

Jika Anda ingin menampilkan waktu sesuai timezone pengguna, lakukan sesudah mounted.

'use client';

import { useEffect, useState } from 'react';

export function LocalTime({ iso, fallbackText }) {
  const [text, setText] = useState(fallbackText);

  useEffect(() => {
    const formatted = new Intl.DateTimeFormat('id-ID', {
      dateStyle: 'medium',
      timeStyle: 'short'
    }).format(new Date(iso));

    setText(formatted);
  }, [iso]);

  return <span>{text}</span>;
}

Server mengirim fallbackText stabil, misalnya UTC atau ISO yang sudah dipadatkan. Client memperbarui setelah hydration selesai.

Client-only untuk area streaming

Jika output AI datang token demi token, jangan SSR token parsial lalu berharap client cocok persis. Lebih aman SSR placeholder, lalu mount komponen streaming di client.

import dynamic from 'next/dynamic';

const StreamingAnswer = dynamic(() => import('./StreamingAnswer'), {
  ssr: false
});

export default function AnswerSection() {
  return (
    <section>
      <h2>Penjelasan AI</h2>
      <p>Penjelasan interaktif dimuat di browser.</p>
      <StreamingAnswer />
    </section>
  );
}

Gunakan pola ini selektif. Jika penjelasan AI adalah konten utama yang ingin terindeks mesin pencari, pertimbangkan SSR hasil final, bukan streaming.

Contoh implementasi di Nuxt.js

Ambil data di server, gunakan payload yang sama di client

Di Nuxt, pola amannya tetap serupa: data untuk SSR dan hydration harus berasal dari payload yang sama, bukan hasil perhitungan ulang di komponen.

<script setup>
const { data } = await useAsyncData('ai-math-result', async () => {
  const result = await $fetch('/api/ai-math-result');

  return {
    id: result.id,
    generatedAtIso: result.generatedAtIso,
    view: {
      summary: result.summaryText,
      confidenceLabel: result.confidenceLabel,
      formattedScore: result.formattedScore,
      explanationHtml: result.explanationHtml
    }
  };
});
</script>

<template>
  <section v-if="data">
    <p><strong>Ringkasan:</strong> {{ data.view.summary }}</p>
    <p><strong>Confidence:</strong> {{ data.view.confidenceLabel }} ({{ data.view.formattedScore }})</p>
    <div v-html="data.view.explanationHtml" />
  </section>
</template>

Hindari computed property yang menghitung ulang formatting penting dari data mentah jika hasilnya sudah tersedia pada payload server.

Mengisolasi komponen non-deterministik

Untuk komponen yang hanya masuk akal di browser, gunakan boundary client-only.

<template>
  <section>
    <h2>Visualisasi Confidence</h2>
    <ClientOnly>
      <ConfidenceChart :points="points" />
      <template #fallback>
        <p>Grafik dimuat di browser.</p>
      </template>
    </ClientOnly>
  </section>
</template>

Fallback ini penting agar SSR tetap menghasilkan markup stabil dan ramah aksesibilitas.

Strategi praktis yang biasanya efektif

Pilih satu sumber kebenaran untuk render awal

Jika server sudah memutuskan string yang tampil, client render pertama harus memakai string itu. Jangan mengambil data mentah yang sama lalu membangun UI dari nol dengan asumsi hasilnya akan identik.

Simpan formatter output, bukan hanya input

Untuk angka, waktu, badge, dan summary singkat, sering kali lebih aman menyimpan:

  • nilai mentah, untuk logika lanjutan
  • nilai terformat, untuk SSR/hydration awal

Ini memang menambah ukuran payload, tetapi mengurangi mismatch dan kompleksitas.

Terapkan seed tetap bila komputasi client tidak bisa dihindari

Bila komponen client harus menghitung ulang, pastikan semua input deterministik tersedia:

  • seed acak tetap
  • timezone dan locale eksplisit
  • opsi pembulatan yang sama
  • urutan input yang stabil

Namun jika hasilnya memengaruhi markup utama, tetap lebih aman mengirim hasil final dari server.

Jangan parse markdown atau math dua kali dengan pipeline berbeda

Jika server merender markdown menjadi HTML, client sebaiknya tidak memproses markdown mentah yang sama dengan parser lain pada render awal. Pilih salah satu:

  • server merender HTML final dan client menampilkan HTML yang sama
  • atau area tersebut client-only dengan fallback SSR yang stabil

Trade-off UX dan SEO

Kapan memilih SSR penuh

Pilih SSR penuh jika:

  • konten AI adalah isi utama halaman
  • Anda ingin konten terbaca tanpa JavaScript
  • SEO dan preview sosial penting
  • hasil AI sudah final sebelum halaman dikirim

Konsekuensinya, Anda harus disiplin menjaga determinisme output awal.

Kapan memilih placeholder atau client-only

Pilih placeholder stabil atau client-only jika:

  • hasil AI masih streaming
  • widget sangat bergantung API browser
  • output berubah cepat dan sulit disamakan dengan SSR
  • bagian tersebut bukan konten SEO utama

Konsekuensinya, pengalaman awal mungkin kurang informatif, tetapi mismatch lebih mudah dikendalikan.

Kompromi yang sering paling masuk akal

Untuk banyak aplikasi, pola terbaik adalah:

  • SSR untuk judul, ringkasan final, metadata, dan hasil yang stabil
  • client-only untuk visualisasi live, streaming token, atau personalisasi lokal pengguna

Dengan begitu, halaman tetap cepat dibaca dan tetap aman saat hydration.

Checklist debugging mismatch hydration

Jika mismatch masih terjadi, periksa hal-hal berikut secara sistematis:

  1. Bandingkan HTML server dan render awal client. Cari teks, atribut, atau urutan node yang berubah.
  2. Cari penggunaan waktu dan angka dinamis. Audit Date.now(), new Date(), toLocaleString(), dan formatter serupa di render path.
  3. Cari randomness. Audit Math.random(), UUID acak, atau sorting tanpa tie-breaker stabil.
  4. Periksa state turunan. Apakah label, summary, atau confidence dihitung ulang di client?
  5. Periksa perbedaan locale/timezone. Pastikan konfigurasi eksplisit jika formatting harus sama di dua sisi.
  6. Periksa parser/renderer kaya-format. Markdown, math, sanitizer, atau syntax highlighter bisa menghasilkan DOM berbeda.
  7. Periksa fetch ganda saat hydration. Client mungkin mengambil ulang data dan menerima hasil terbaru sebelum hydration selesai.
  8. Periksa key daftar. Key yang tidak stabil menyebabkan urutan node berbeda.
  9. Uji tanpa ekstensi browser. Kadang alat pihak ketiga mengubah DOM.
  10. Isolasi komponen tersangka. Ubah sementara menjadi placeholder statis untuk memastikan sumber mismatch.

Teknik logging yang membantu

Saat debugging, log payload yang dipakai server dan payload yang dibaca client. Fokus pada data yang langsung memengaruhi markup:

  • string summary
  • formattedScore
  • timestamp yang sudah diformat
  • urutan item rekomendasi
  • HTML hasil markdown

Jika nilainya sama tetapi mismatch tetap muncul, besar kemungkinan masalah ada pada renderer HTML, conditional rendering, atau key daftar.

Kesalahan implementasi yang sering terjadi

  • Mengirim data mentah saja, lalu membiarkan client menghitung ulang semua label presentasional.
  • Merender waktu lokal saat SSR tanpa timezone eksplisit.
  • Menggabungkan streaming server dan refetch client pada area yang sama tanpa sinkronisasi state awal.
  • Menggunakan placeholder dinamis yang berubah sebelum hydration selesai.
  • Menganggap mismatch kecil aman; pada UI interaktif, efek lanjutannya bisa sulit didiagnosis.

Penutup

Pada UI yang menampilkan hasil AI atau komputasi yang mudah berubah, SSR deterministik untuk UI AI bergantung pada satu disiplin utama: render awal harus berasal dari data yang sama persis di server dan client. Jangan hitung ulang summary, confidence, formatting, atau output kaya-format pada hydration jika hasilnya bisa berubah.

Secara praktis, pola yang paling aman adalah: siapkan view model deterministik di server, serialisasikan sebagai state awal, gunakan placeholder stabil untuk area yang belum final, dan pindahkan komponen yang benar-benar non-deterministik ke boundary client-only. Dengan pendekatan ini, Anda bisa menjaga keseimbangan antara UX, SEO, dan keandalan hydration tanpa mengorbankan fitur AI di antarmuka.