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
- Buat halaman SSR dengan satu cabang UI berdasarkan flag AI.
- Render server menggunakan default
false. - Render klien membaca
localStorageyang bisa bernilaitrue. - Muat halaman dengan
localStorage['ai-enabled']='1'. - 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:
- Server membaca cookie, session, header, region, dan konfigurasi rollout.
- Server menghasilkan objek initialFlags.
- Objek itu diserialisasi ke halaman.
- Klien menginisialisasi store/state dari snapshot tersebut.
- 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 serverMengapa 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, ataulocalStoragedi 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 windowhanya 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.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!