Ketika tim produk AI bergerak cepat, flag fitur sering berubah beberapa kali dalam sehari: model baru dinyalakan untuk sebagian pengguna, eksperimen prompt diaktifkan per region, atau komponen rekomendasi AI ditampilkan hanya untuk akun tertentu. Dalam aplikasi SSR, perubahan semacam ini mudah memicu render mismatch dan hydration issue bila HTML dari server tidak identik dengan render awal di browser.

Masalah inti debug render mismatch SSR saat flag AI mengubah UI klien biasanya sederhana: server merender UI berdasarkan satu nilai flag, lalu klien menghitung ulang dengan nilai berbeda. Akibatnya, framework mencoba menghidrasi DOM yang tidak sesuai. Hasilnya bisa berupa warning, flicker, event handler tidak terpasang konsisten, atau komponen diganti penuh di klien.

Konteks startup AI saat ini memang mendorong perubahan fitur yang sangat cepat: rollout model, gating per negara, fallback provider, dan A/B test antarmuka. Namun pada SSR, kecepatan perubahan fitur harus diimbangi disiplin menjaga kesamaan state awal antara server dan klien.

Apa itu render mismatch dan hydration issue pada SSR

Pada SSR, server mengirim HTML awal agar halaman cepat tampil dan mudah diindeks. Setelah itu, JavaScript di klien melakukan hydration: framework menghubungkan event listener dan state ke DOM yang sudah ada. Hydration mengasumsikan bahwa hasil render pertama di klien sama dengan HTML dari server.

Jika asumsi ini rusak, framework dapat menampilkan gejala seperti:

  • Warning seperti Text content did not match atau Hydration completed but contains mismatches.
  • Elemen UI AI muncul di server tetapi hilang di klien, atau sebaliknya.
  • Flicker saat komponen diganti ulang setelah JavaScript aktif.
  • State interaktif aneh: tombol tidak responsif, input kehilangan nilai, atau event handler terpasang ke struktur DOM yang berbeda.

Masalah ini sering tampak “acak”, padahal sumbernya hampir selalu deterministik: ada nilai yang berbeda antara proses render di server dan proses render pertama di browser.

Gejala khas saat flag AI memengaruhi UI

Ketika fitur AI dikendalikan oleh flag atau konfigurasi runtime, pola gejalanya biasanya lebih spesifik:

  • Komponen AI hanya muncul untuk sebagian user, tetapi server belum tahu identitas atau segmentasi user saat render.
  • Fallback model berbeda antara server dan klien karena konfigurasi region dibaca dari sumber berbeda.
  • Eksperimen A/B ditentukan ulang di browser sehingga varian UI tidak sama dengan server.
  • UI bergantung pada storage lokal, misalnya pengguna pernah menonaktifkan fitur AI lewat localStorage, tetapi server tidak bisa membaca nilai itu.
  • Waktu, locale, atau timezone memengaruhi teks AI, timestamp, atau format angka.

Contoh yang sering terjadi: server menampilkan panel “AI Assistant aktif” karena flag global bernilai true, tetapi klien membaca preferensi lokal pengguna bahwa fitur itu dimatikan. Hasilnya, struktur DOM awal berbeda.

Root cause umum

1. State awal server dan klien berbeda

Ini penyebab paling umum. Server merender dengan data dari cookie, header, session, atau environment. Klien lalu merender dengan data dari localStorage, API browser, atau request ulang ke endpoint flag. Jika sumber data dan urutan inisialisasinya tidak sama, mismatch hampir pasti terjadi.

Anti-pattern: menghitung flag langsung saat render dari sumber yang hanya tersedia di browser.

// Anti-pattern umum di komponen SSR-capable
const aiEnabled = localStorage.getItem('ai-enabled') === '1';

return aiEnabled ? '<AiPanel />' : '<BasicPanel />';

Server tidak punya localStorage, sehingga render server dan klien akan berbeda atau bahkan error.

2. Akses window, document, atau localStorage saat render

Walau kode dibungkus pengecekan seperti typeof window !== 'undefined', masalah belum selesai. Pengecekan itu mencegah crash di server, tetapi nilai render tetap bisa berbeda.

// Tidak crash di server, tetapi tetap berpotensi mismatch
const aiEnabled = typeof window !== 'undefined'
  ? window.localStorage.getItem('ai-enabled') === '1'
  : false;

Server selalu merender false; klien bisa merender true pada pass pertama. Inilah mismatch.

3. Timezone dan locale

UI AI kadang menampilkan waktu generasi, estimasi durasi, atau format angka/token. Jika server menggunakan locale default tertentu dan browser memakai locale pengguna, output teks bisa berbeda walau datanya sama.

// Berpotensi menghasilkan string berbeda
new Date(createdAt).toLocaleString();

Perbedaan ini cukup untuk memicu warning hydration pada node teks.

4. Random value atau ID yang dibuat saat render

ID acak untuk komponen, eksperimen, atau seed personalisasi AI tidak boleh dibuat langsung saat render SSR bila hasilnya perlu cocok di klien.

// Anti-pattern
const experimentBucket = Math.random() > 0.5 ? 'A' : 'B';

Server dan klien hampir pasti memilih nilai berbeda.

5. Async fetch yang hasilnya berbeda

Server mungkin mengambil konfigurasi flag dari edge config atau service internal, sedangkan klien memanggil endpoint publik dengan cache, header, atau kredensial berbeda. Meski nama endpoint sama, respons awal bisa tidak identik.

Kasus umum lainnya: server mendapat user dari cookie HttpOnly, sedangkan klien belum punya hasil fetch profil saat render awal.

6. A/B flag dan rollout bertahap

Eksperimen AI sering ditentukan berdasarkan user ID, region, atau persentase rollout. Jika bucketing dilakukan sekali di server dan sekali lagi di klien dengan input berbeda, UI akan berganti saat hydration.

Contohnya:

  • Server memakai cookie anonymous ID.
  • Klien memakai ID dari storage yang belum tentu sama.
  • Server tidak tahu timezone/region final karena berada di balik proxy yang tidak meneruskan header relevan.

Cara mereproduksi bug secara sengaja

Sebelum memperbaiki, buat bug menjadi stabil. Tujuannya agar Anda bisa membuktikan akar masalah, bukan menebak-nebak.

Skenario reproduksi minimal

  1. Buat halaman SSR dengan satu cabang UI berdasarkan flag AI.
  2. Render server menggunakan default false.
  3. Render klien membaca localStorage yang bisa bernilai true.
  4. Muat halaman dengan localStorage['ai-enabled']='1'.
  5. Amati warning hydration dan perubahan DOM.
// Contoh pseudo-code lintas framework
const initialFlag = false; // hasil SSR
const clientFlag = browser ? localStorage.getItem('ai-enabled') === '1' : initialFlag;

return clientFlag ? '<AiComposer />' : '<StandardComposer />';

Untuk reproduksi yang lebih realistis, tambahkan salah satu faktor berikut:

  • Format tanggal dengan locale browser.
  • Bucket eksperimen berbasis random.
  • Fetch flag dari endpoint klien yang terlambat beberapa ratus milidetik.

Data yang perlu dicatat saat reproduksi

  • HTML hasil SSR yang diterima browser.
  • Nilai flag pada server.
  • Nilai flag pada render pertama di klien.
  • Apakah ada perubahan sesudah mount.
  • Cookie, header, locale, timezone, dan identity yang dipakai untuk evaluasi flag.

Checklist debugging yang benar-benar berguna

1. Bandingkan sumber kebenaran flag

Tanyakan dengan eksplisit:

  • Di server, flag dibaca dari mana?
  • Di klien, flag dibaca dari mana?
  • Apakah keduanya memakai input identik?
  • Apakah nilai itu tersedia sebelum render pertama?

Jika jawabannya “tidak”, Anda sudah menemukan kandidat akar masalah.

2. Log nilai pada dua sisi

Tambahkan logging sementara yang mudah dikorelasikan dengan satu request.

// Server
console.log('[SSR] aiEnabled=', aiEnabled, 'userId=', userId, 'locale=', locale);

// Client
console.log('[HYDRATE] aiEnabled=', aiEnabled, 'userId=', userId, 'locale=', navigator.language);

Jangan hanya log hasil akhir. Log juga input evaluasi flag: user ID, segment, region, locale, eksperimen, dan sumber data.

3. Lihat HTML mentah sebelum JavaScript berjalan

Buka View Source atau matikan JavaScript sementara. Tujuannya untuk memastikan apa yang benar-benar dirender server, bukan DOM yang sudah dimodifikasi browser atau framework.

4. Cari nilai non-deterministik saat render

Audit komponen SSR untuk:

  • Math.random()
  • Date.now()
  • new Date().toLocaleString()
  • window, document, localStorage
  • Fetch langsung dari browser saat render awal

5. Pastikan identitas user konsisten

Banyak mismatch terjadi bukan karena flag-nya salah, tetapi karena user identity berbeda. Server mungkin menganggap user anonim, klien menganggap sudah login setelah token dimuat, lalu UI AI berubah total.

6. Nonaktifkan eksperimen sementara

Untuk isolasi, paksa semua request ke satu varian. Jika mismatch hilang, berarti masalah ada di evaluasi A/B atau rollout.

7. Uji di environment yang meniru produksi

Perbedaan timezone server, CDN cache, atau edge runtime sering tidak muncul di localhost. Uji di staging dengan header, cookie, dan region yang menyerupai produksi.

Pola perbaikan yang aman

1. Kirim snapshot flag dari server ke klien

Pola paling aman adalah mengevaluasi flag sekali di server untuk request tersebut, lalu mengirim snapshot yang sama ke klien sebagai state awal. Dengan begitu, render pertama di klien memakai nilai yang identik dengan SSR.

Prinsipnya:

  1. Server membaca cookie, session, header, region, dan konfigurasi rollout.
  2. Server menghasilkan objek initialFlags.
  3. Objek itu diserialisasi ke halaman.
  4. Klien menginisialisasi store/state dari snapshot tersebut.
  5. Pembaruan flag boleh dilakukan setelah mount, dengan transisi UI yang disengaja.
// Pseudo-code umum
// server
const initialFlags = evaluateFlags(requestContext);
renderPage({ initialFlags });

// client
const flags = createFlagStore(window.__INITIAL_FLAGS__);
// render pertama memakai nilai yang sama dengan server

Mengapa ini bekerja? Karena sumber kebenaran render awal hanya satu: snapshot server.

2. Tunda akses API browser sampai setelah mount

Jika preferensi AI memang hanya ada di browser, jangan dipakai untuk menentukan HTML SSR. Render placeholder atau default yang stabil, lalu sinkronkan setelah komponen ter-mount.

// Pola aman secara umum
let mounted = false;
let aiEnabled = initialFlags.aiEnabled;

onMount(() => {
  mounted = true;
  const localPref = localStorage.getItem('ai-enabled');
  if (localPref != null) {
    aiEnabled = localPref === '1';
  }
});

Trade-off: ada kemungkinan perubahan UI setelah mount. Namun ini lebih baik daripada hydration mismatch. Untuk mengurangi flicker, gunakan placeholder netral atau simpan preferensi yang sama di cookie agar server juga bisa membacanya.

3. Gunakan cookie untuk preferensi yang memengaruhi SSR

Jika preferensi AI perlu memengaruhi HTML awal, simpan nilainya di cookie yang dapat diakses server. Ini lebih konsisten daripada hanya di localStorage.

Trade-off: cookie menambah kompleksitas sinkronisasi dan harus dipikirkan dari sisi ukuran, privasi, dan keamanan.

4. Stabilkan locale dan timezone

Untuk konten yang sensitif terhadap formatting, pilih salah satu:

  • Kirim string yang sudah diformat dari server dan jangan format ulang saat hydration.
  • Gunakan locale/timezone yang eksplisit dan sama di kedua sisi.
  • Jika formatting benar-benar harus spesifik browser, render placeholder di SSR lalu isi setelah mount.
// Lebih stabil daripada mengandalkan default environment
new Intl.DateTimeFormat('id-ID', {
  timeZone: 'UTC',
  dateStyle: 'medium',
  timeStyle: 'short'
}).format(new Date(createdAt));

5. Hindari random saat render, pakai seed stabil

Untuk bucketing eksperimen, gunakan hash dari identifier stabil seperti user ID atau anonymous ID yang sama di server dan klien. Jangan memakai Math.random() di render.

// Pseudo-code
const bucket = stableHash(userKey + ':ai-composer') % 100;
const inExperiment = bucket < rolloutPercentage;

Mengapa ini bekerja? Karena hasilnya deterministik selama input sama.

6. Pisahkan komponen client-only bila memang tidak perlu SSR

Jika widget AI tertentu tidak penting untuk HTML awal, jadikan komponen client-only. Ini menghindari kontrak “HTML harus sama” untuk bagian itu.

Trade-off: SEO, perceived performance, dan first paint untuk widget tersebut bisa menurun. Gunakan hanya ketika SSR memang tidak memberi manfaat besar.

Contoh anti-pattern dan perbaikannya

Anti-pattern: flag AI dibaca langsung dari localStorage saat render

// Anti-pattern umum
function ChatEntry() {
  const aiEnabled = typeof window !== 'undefined'
    && localStorage.getItem('ai-enabled') === '1';

  return aiEnabled ? '<AiEntry />' : '<BasicEntry />';
}

Masalahnya: server akan merender BasicEntry, klien bisa merender AiEntry pada pass pertama.

Perbaikan: gunakan initial flag dari server

// Pseudo-code framework-agnostic
function ChatEntry({ initialAiEnabled }) {
  let aiEnabled = initialAiEnabled;

  onMount(() => {
    const saved = localStorage.getItem('ai-enabled');
    if (saved != null) {
      aiEnabled = saved === '1';
    }
  });

  return aiEnabled ? '<AiEntry />' : '<BasicEntry />';
}

Dengan pola ini, SSR dan render awal klien tetap sama. Perubahan sesudah mount menjadi transisi state biasa, bukan mismatch hydration.

Panduan umum di Next.js, Nuxt, dan SvelteKit

Next.js

Di Next.js secara umum, evaluasi flag sebaiknya dilakukan di sisi server untuk request yang sedang diproses, lalu diteruskan sebagai props atau state awal. Hindari membaca window atau localStorage untuk menentukan cabang render yang ikut SSR.

  • Jika komponen AI harus SSR, kirim initialFlags dari server.
  • Jika komponen AI tidak penting untuk SSR, pertimbangkan client-only atau dynamic import tanpa SSR untuk bagian tersebut.
  • Pastikan data user dan eksperimen yang dipakai server juga tersedia identik di klien.

Nuxt

Di Nuxt, prinsipnya sama: gunakan data awal yang dihasilkan server dan injeksikan ke state universal. Hindari mengambil keputusan render awal berdasarkan API browser. Bila perlu, pisahkan komponen yang memang hanya layak dijalankan di klien.

  • Tempatkan evaluasi flag pada lapisan server/request-aware.
  • Gunakan store/state yang dihidrasi dari payload server.
  • Jangan format konten sensitif locale dengan default environment yang berbeda.

SvelteKit

Di SvelteKit, evaluasi flag cocok dilakukan pada load server-side atau hook yang punya akses ke request context, lalu hasilnya dipakai sebagai data halaman. Akses browser-only API sebaiknya dilakukan setelah mount untuk menghindari perbedaan output awal.

  • Gunakan data dari server sebagai basis render pertama.
  • Jaga agar branch SSR tidak tergantung pada browser-only state.
  • Jika perlu sinkronisasi setelah mount, buat transisinya eksplisit dan dapat diuji.

Checklist rilis agar UI SSR tetap konsisten

  • Apakah semua flag yang memengaruhi HTML awal dievaluasi di server?
  • Apakah klien menerima snapshot flag yang sama untuk render pertama?
  • Apakah ada akses window, document, atau localStorage di jalur render SSR?
  • Apakah ada Math.random(), Date.now(), atau formatting locale implicit saat render?
  • Apakah A/B bucket menggunakan seed stabil yang sama di server dan klien?
  • Apakah preferensi pengguna yang memengaruhi SSR disimpan di cookie atau sumber yang dapat dibaca server?
  • Apakah HTML server sudah diperiksa tanpa JavaScript?
  • Apakah staging diuji dengan timezone, locale, cookie, dan region yang menyerupai produksi?
  • Apakah komponen client-only benar-benar tidak perlu SSR?
  • Apakah warning hydration dipantau di console, log frontend, atau error tracker?

Kesalahan yang sering dilakukan tim

  • Menyamakan “tidak crash” dengan “aman untuk SSR”. Pengecekan typeof window hanya mencegah error, bukan mismatch.
  • Membiarkan flag dievaluasi dua kali. Sekali di server, sekali di klien, dengan input berbeda.
  • Mengandalkan default environment. Locale, timezone, dan format tanggal default sering tidak sama.
  • Menggunakan localStorage sebagai sumber utama untuk UI SSR. Ini nyaman, tetapi server tidak bisa melihatnya.
  • Meremehkan efek cache. Respons flag atau payload user dari CDN dapat berbeda dari ekspektasi request saat ini.

Penutup

Debug render mismatch SSR saat flag AI mengubah UI klien pada dasarnya adalah pekerjaan menyamakan input render pertama antara server dan browser. Jika HTML awal harus konsisten, maka flag, user identity, locale, eksperimen, dan data AI yang memengaruhi struktur UI juga harus konsisten.

Pola yang paling aman adalah: evaluasi flag di server, kirim snapshot ke klien, hindari sumber data browser saat render SSR, dan pindahkan sinkronisasi yang tidak deterministik ke fase setelah mount. Dengan pendekatan ini, tim tetap bisa bergerak cepat melakukan rollout fitur AI tanpa membuat hydration issue menjadi bug produksi yang sulit dilacak.