Mendiagnosis SSR mismatch dari data waktu dan locale di frontend biasanya berujung pada satu pola: HTML hasil render di server tidak identik dengan hasil render pertama di browser. Dampaknya terlihat sebagai warning hydration, teks tanggal yang berubah setelah mount, UI berkedip, atau snapshot HTML yang tidak konsisten antara environment.
Kasus ini sering terasa membingungkan karena kode tampak benar secara lokal, tetapi gagal di container, staging, atau production. Penyebab umumnya bukan framework semata, melainkan render yang tidak deterministik: penggunaan Date.now(), new Date(), Intl.DateTimeFormat dengan locale default yang berbeda, timezone server/container yang tidak sama dengan browser, atau format angka/tanggal yang bergantung pada environment.
Apa yang sebenarnya terjadi saat hydration
Pada aplikasi SSR, server lebih dulu menghasilkan HTML. Di browser, framework kemudian melakukan hydration, yaitu menghubungkan HTML yang sudah ada dengan komponen dan event handler di sisi klien. Agar proses ini bersih, output render awal di browser harus sama dengan HTML yang dihasilkan server.
Begitu ada perbedaan string sekecil apa pun, misalnya:
- server merender
12/03/2025tetapi browser merender3/12/2025, - server merender
08:00tetapi browser merender15:00, - server memakai locale
en-USdan browser memakaiid-ID,
framework dapat memunculkan warning hydration, mengganti node DOM, atau memicu rerender yang terlihat sebagai flicker.
Intinya: jika nilai yang dirender bergantung pada waktu saat ini, timezone lokal, locale default, atau formatter yang hasilnya berubah antar-environment, SSR mismatch sangat mungkin terjadi.
Gejala nyata yang sering muncul
1. Warning hydration di console
Pada React/Next.js, gejalanya sering berupa pesan tentang teks yang tidak cocok antara server dan client. Pada Nuxt/Vue, gejalanya serupa: node atau text content hasil hydration tidak sama dengan markup SSR.
2. Teks tanggal atau angka berubah setelah mount
Contoh paling umum: halaman menampilkan tanggal dalam satu format saat HTML pertama muncul, lalu berubah sepersekian detik setelah JavaScript aktif. Pengguna melihat perubahan ini sebagai flicker.
3. Snapshot HTML tidak sama
Jika Anda membandingkan View Source, hasil SSR, atau output server dengan DOM setelah hydration, nilainya berbeda. Ini sering terlihat saat menjalankan snapshot test atau E2E test lintas timezone.
4. Bug hanya muncul di production
Lokal sering memakai timezone mesin developer dan locale browser yang konsisten. Di production, server bisa berjalan di UTC, image container mungkin minim data locale, dan browser pengguna memakai locale berbeda.
Root cause umum mismatch waktu dan locale
Penggunaan Date.now() atau new Date() langsung di render
Ini adalah penyebab paling sering. Jika Anda memanggil waktu saat ini saat render di server dan browser, nilainya hampir pasti berbeda walau hanya beberapa milidetik.
function Timestamp() {
return <span>{new Date().toISOString()}</span>;
}Server menghasilkan timestamp A, browser menghasilkan timestamp B. Hasilnya mismatch.
Formatter Intl tanpa locale/timezone eksplisit
Formatter seperti Intl.DateTimeFormat atau toLocaleString() bergantung pada default environment jika Anda tidak memberi parameter eksplisit.
function PublishedAt({ date }) {
return <span>{new Date(date).toLocaleString()}</span>;
}Kode di atas berbahaya untuk SSR karena:
- locale default server mungkin berbeda dari browser,
- timezone server bisa UTC, sementara browser pengguna di Asia/Jakarta,
- dukungan ICU/locale pada runtime server bisa berbeda.
Timezone container atau host server berbeda
Banyak deployment server berjalan dengan timezone UTC. Browser pengguna menggunakan timezone lokal perangkat. Jika render awal memakai timezone default masing-masing, tanggal dan jam bisa berbeda beberapa jam, bahkan berpindah hari.
Locale default server berbeda dari browser
Browser pengguna Indonesia mungkin memilih id-ID, sedangkan server default ke en-US atau locale lain. Akibatnya:
- urutan tanggal berubah,
- nama bulan berbeda bahasa,
- format angka desimal/ribuan berubah.
Nilai non-deterministik lain ikut dirender
Selain waktu, pola yang sama terjadi pada nilai acak atau bergantung environment, misalnya:
Math.random()saat render,- ID unik yang dibuat saat render tanpa mekanisme SSR-safe,
- akses ke preferensi browser yang belum tersedia di server.
Walau fokus artikel ini adalah waktu dan locale, prinsip diagnosisnya sama: jangan render nilai yang tidak identik antara server dan client pada pass pertama.
Cara mereproduksi masalah dengan cepat
Jika bug sulit ditangkap, pakai checklist reproduksi berikut.
Checklist environment
- Jalankan aplikasi dengan timezone server berbeda dari timezone browser.
- Ubah locale browser, misalnya dari
en-USkeid-ID. - Jalankan aplikasi di container dengan timezone default UTC.
- Bandingkan hasil SSR dengan DOM setelah hydration.
Contoh reproduksi sederhana
function EventTime({ iso }) {
return (
<p>
{new Intl.DateTimeFormat(undefined, {
dateStyle: 'long',
timeStyle: 'short'
}).format(new Date(iso))}
</p>
);
}Kode ini tampak normal, tetapi undefined berarti locale mengikuti default environment. Timezone juga default. Jika server dan browser berbeda, string hasil format juga berbeda.
Bandingkan tiga hal berikut
- HTML server: hasil yang dikirim sebelum JavaScript berjalan.
- Render awal client: nilai yang framework hasilkan saat hydration.
- Render setelah mount: apakah ada perubahan tambahan dari effect atau state.
Jika mismatch terjadi antara poin pertama dan kedua, masalah ada pada SSR/hydration. Jika perbedaan baru muncul setelah mount, Anda mungkin sengaja melakukan reformat di sisi klien.
Langkah debugging yang praktis
1. Audit semua render yang memakai tanggal, waktu, dan formatter locale
Cari pemanggilan berikut di komponen yang ikut SSR:
new Date()Date.now()toLocaleDateString()toLocaleString()Intl.DateTimeFormat(...)Intl.NumberFormat(...)
Jika dipanggil langsung di body komponen atau template SSR, tandai sebagai kandidat mismatch.
2. Log nilai mentah dan hasil format di server dan client
Jangan hanya melihat output final. Catat juga input dan konfigurasi formatter.
const date = new Date(iso);
const formatted = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'long',
timeStyle: 'short',
timeZone: 'Asia/Jakarta'
}).format(date);
console.log({
iso,
timestamp: date.getTime(),
formatted
});Jika timestamp sama tetapi string format berbeda, masalah ada pada locale/timezone/formatter. Jika timestamp sendiri berbeda, ada sumber waktu dinamis di render.
3. Periksa timezone proses server
Pastikan Anda tahu timezone default proses runtime server atau container. Banyak kasus selesai hanya dengan menyadari server berjalan di UTC sementara developer mengira mengikuti timezone lokal.
4. Bandingkan output dengan locale dan timezone eksplisit
Jika mismatch hilang setelah Anda menetapkan locale dan timezone secara eksplisit, berarti akar masalahnya memang default environment.
5. Uji tanpa SSR pada komponen bermasalah
Ini bukan solusi utama, tetapi berguna untuk isolasi. Jika warning hilang saat komponen hanya dirender di client, penyebab hampir pasti berasal dari output SSR yang tidak deterministik.
Strategi perbaikan yang paling aman
Strategi 1: Kirim nilai mentah yang stabil, format di client setelah mount
Pendekatan ini cocok jika output memang harus mengikuti locale/timezone pengguna. Server cukup mengirim ISO string atau timestamp, lalu browser memformat setelah mount.
function LocalizedTime({ iso }) {
const [text, setText] = React.useState('');
React.useEffect(() => {
const value = new Intl.DateTimeFormat(navigator.language, {
dateStyle: 'long',
timeStyle: 'short'
}).format(new Date(iso));
setText(value);
}, [iso]);
return <span>{text || iso}</span>;
}Mengapa ini bekerja: output SSR tidak lagi bergantung pada locale/timezone browser. Nilai awal stabil, lalu browser mengganti dengan format lokal setelah hydration selesai.
Trade-off:
- ada potensi perubahan teks setelah mount,
- bisa menimbulkan flicker jika fallback kurang baik,
- SEO untuk teks terformat lokal mungkin tidak ideal.
Tips: gunakan placeholder yang konsisten, misalnya ISO, tanggal pendek yang netral, atau skeleton.
Strategi 2: Tetapkan locale dan timezone secara eksplisit di server dan client
Jika Anda membutuhkan output yang sama persis saat SSR dan hydration, gunakan locale dan timezone yang sama di kedua sisi.
const formatter = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'long',
timeStyle: 'short',
timeZone: 'Asia/Jakarta'
});
function EventTime({ iso }) {
return <span>{formatter.format(new Date(iso))}</span>;
}Mengapa ini bekerja: Anda menghilangkan ketergantungan pada default environment. Selama runtime server mendukung locale tersebut dan input sama, output akan jauh lebih konsisten.
Trade-off:
- format tidak otomatis mengikuti locale pengguna,
- perlu kebijakan jelas: apakah waktu ditampilkan dalam timezone bisnis, UTC, atau timezone user,
- dukungan locale server tetap perlu diperhatikan.
Strategi 3: Format di server, kirim hasil string final sebagai data
Jika aplikasi punya backend/API yang sudah mengetahui locale dan timezone yang harus dipakai, string final bisa disiapkan di sana. Frontend hanya menampilkan string itu.
Cocok untuk:
- halaman marketing atau konten statis,
- kasus yang mengutamakan HTML final stabil,
- format yang tidak perlu berubah mengikuti browser.
Keterbatasan: lebih kaku jika user ingin preferensi lokal perangkat.
Strategi 4: Tunda render bagian yang non-deterministik sampai client-ready
Untuk elemen yang memang harus bergantung pada waktu saat ini atau timezone browser, render komponen itu hanya setelah client siap.
function ClientOnlyTime({ iso }) {
const [ready, setReady] = React.useState(false);
React.useEffect(() => setReady(true), []);
if (!ready) {
return <span>--</span>;
}
return (
<span>
{new Intl.DateTimeFormat(navigator.language, {
dateStyle: 'long',
timeStyle: 'short'
}).format(new Date(iso))}
</span>
);
}Kapan dipilih: jika nilai awal dari server memang tidak bermakna atau hampir pasti salah bagi user.
Kekurangan: konten tidak hadir penuh saat SSR.
Pola yang sebaiknya dihindari
Jangan render waktu saat ini langsung di JSX/template SSR
<div>Diproses pada {new Date().toLocaleString()}</div>Ini hampir selalu mismatch.
Jangan mengandalkan locale default
new Intl.DateTimeFormat().format(date)Tanpa locale dan timezone eksplisit, hasilnya bisa berubah antar mesin.
Jangan mencampur waktu server dan waktu browser tanpa aturan
Contoh klasik: server mengirim ISO UTC, tetapi browser memformat dengan timezone lokal tanpa menandai bahwa nilai tersebut memang dimaksudkan lokal. Ini bukan selalu salah, tetapi harus menjadi keputusan sadar.
Contoh perbaikan dari kasus nyata
Kasus bermasalah
function OrderSummary({ createdAt }) {
return (
<p>
Dibuat: {new Date(createdAt).toLocaleString()}
</p>
);
}Masalah:
- locale default tidak eksplisit,
- timezone default tidak eksplisit,
- output bisa beda antara server dan browser.
Perbaikan jika butuh format stabil lintas environment
function OrderSummary({ createdAt }) {
const text = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Asia/Jakarta'
}).format(new Date(createdAt));
return <p>Dibuat: {text}</p>;
}Perbaikan jika harus mengikuti locale user
function OrderSummary({ createdAt }) {
const [text, setText] = React.useState('');
React.useEffect(() => {
setText(
new Intl.DateTimeFormat(navigator.language, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(createdAt))
);
}, [createdAt]);
return <p>Dibuat: {text || createdAt}</p>;
}Pilih pendekatan kedua jika pengalaman lokal pengguna lebih penting daripada kesamaan total antara SSR dan render awal.
Checklist diagnosis untuk React/Next.js atau Nuxt
- Apakah komponen SSR merender
new Date()atauDate.now()? - Apakah ada
toLocaleString()atauIntl.*tanpa locale/timezone eksplisit? - Apakah timezone server/container diketahui dan terdokumentasi?
- Apakah locale browser user diasumsikan sama dengan server?
- Apakah input tanggal berbentuk ISO UTC, local time, atau string ambigu?
- Apakah perbedaan hanya muncul setelah mount, atau sudah ada saat hydration?
- Apakah ada fallback UI yang menimbulkan flicker berlebihan?
- Apakah snapshot test dijalankan dalam timezone dan locale yang dikontrol?
Pencegahan di code review dan testing
Aturan code review yang efektif
- Tandai semua pemanggilan API waktu di body komponen SSR.
- Wajibkan locale dan timezone eksplisit untuk formatter yang dirender saat SSR.
- Pastikan semua tanggal dari API memiliki format yang jelas, idealnya ISO dengan zona waktu eksplisit.
- Larangan praktis: jangan gunakan nilai non-deterministik pada render pertama kecuali memang dirender client-only.
Strategi testing
- Jalankan test dengan timezone tetap agar hasil konsisten.
- Tambahkan test pada locale yang berbeda jika aplikasi multi-locale.
- Bandingkan HTML SSR dengan output hydrated untuk komponen yang sensitif terhadap waktu.
- Uji di container yang menyerupai production, bukan hanya di mesin lokal.
Praktik desain data yang membantu
Simpan dan kirim data waktu sebagai nilai mentah yang tidak ambigu, misalnya ISO 8601 dengan offset atau UTC. Hindari string tanggal bebas format seperti 03/04/2025 10:00 karena bisa ditafsirkan berbeda.
Kapan memilih masing-masing strategi
- Pilih format eksplisit di SSR jika Anda butuh HTML stabil, minim flicker, dan format bisnis tetap.
- Pilih format di client setelah mount jika tampilan harus mengikuti locale/timezone pengguna.
- Pilih string final dari backend jika frontend tidak perlu memutuskan formatting.
- Pilih client-only render untuk nilai yang benar-benar bergantung pada environment browser saat ini.
Penutup
SSR mismatch dari data waktu dan locale hampir selalu berasal dari render awal yang tidak deterministik. Diagnosisnya dimulai dengan pertanyaan sederhana: apakah server dan browser merender input, locale, timezone, dan formatter yang sama?
Jika jawabannya tidak, perbaikannya juga jelas: stabilkan input, tetapkan locale/timezone secara eksplisit, atau tunda formatting ke client bila memang harus mengikuti environment pengguna. Dengan pola ini, warning hydration, teks tanggal yang berubah setelah mount, UI flicker, dan snapshot HTML yang tidak sama biasanya bisa diselesaikan secara sistematis, bukan coba-coba.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!