Remote Browser SSR menjadi sumber masalah yang sering membingungkan: halaman terlihat normal saat diuji di mesin utama, tetapi memunculkan warning hydration, layout bergeser, atau state awal berubah ketika browser dijalankan dari Mac remote untuk QA, automation, atau remote control. Akar masalahnya hampir selalu sama: server-rendered HTML dan hasil render awal di klien tidak deterministik karena environment kedua mesin berbeda.
Artikel ini membahas cara mengenali gejala, mereproduksi bug, mendiagnosis mismatch, dan mengeraskan pipeline agar hasil SSR konsisten antar mesin Mac. Fokusnya bukan pada teori hydration secara umum, tetapi pada kasus nyata ketika rendering atau pengujian dilakukan dari browser remote.
Apa itu hydration drift pada setup remote browser
Pada aplikasi SSR, server mengirim HTML awal. JavaScript di browser lalu melakukan hydration dengan asumsi bahwa struktur dan isi HTML awal sama dengan hasil render komponen di klien untuk state awal yang sama. Jika ada perbedaan, framework akan mengeluarkan warning seperti:
- Text content does not match server-rendered HTML
- Hydration completed but contains mismatches
- Expected server HTML to contain a matching element
Saat browser berjalan di mesin remote, state awal sering tidak lagi sama dengan saat server merender. Perbedaannya bisa halus:
- locale berbeda, sehingga format tanggal/angka berubah
- timezone berbeda, sehingga hari atau jam bergeser
- font berbeda, menyebabkan line-wrap dan ukuran elemen berubah
- viewport berbeda, memicu percabangan render responsif
- extension browser menyuntik DOM atau mengubah perilaku halaman
- user agent berbeda, memicu kode kondisional
- cookie atau session berbeda, sehingga state autentikasi/eksperimen berubah
- feature flag berbeda antara server dan browser remote
- environment variable berbeda antar proses build, server, atau test runner
Hydration drift bukan hanya warning kosmetik. Dalam kasus tertentu, event handler bisa menempel pada node yang salah, state awal meloncat, visual snapshot menjadi flakey, dan bug hanya muncul di CI atau QA remote.
Gejala yang umum muncul
1. Warning hydration yang tidak konsisten
Bug tidak muncul di laptop developer A, tetapi selalu muncul di spare Mac untuk remote browser atau di runner CI. Ini biasanya tanda bahwa input render bergantung pada environment mesin.
2. Snapshot visual berbeda padahal data sama
Jika HTML awal sama tetapi font, viewport, atau zoom berbeda, hasil visual bisa bergeser. Ini bukan selalu hydration mismatch, tetapi sering berkorelasi karena percabangan UI juga ikut berubah.
3. Konten awal berubah sesaat setelah halaman tampil
Contoh khas: server menampilkan tanggal 31/12/2024, lalu setelah hydration berubah menjadi 12/31/2024. Perubahan sesaat seperti ini hampir selalu berasal dari formatting yang bergantung pada locale atau timezone browser.
4. Bug hanya terjadi pada akun, cookie, atau flag tertentu
Server mungkin merender satu varian fitur, tetapi browser remote membaca cookie atau local storage berbeda lalu memilih varian lain saat hydrate.
Penyebab utama mismatch antar mesin Mac
Locale dan timezone
Ini penyebab paling umum. API seperti Intl.DateTimeFormat, toLocaleString(), atau parsing tanggal tanpa timezone eksplisit dapat menghasilkan output berbeda pada mesin yang berbeda.
// Buruk: output tergantung locale dan timezone runtime saat ini
const label = new Date(order.createdAt).toLocaleString();Jika server merender dengan timezone UTC dan browser remote memakai Asia/Jakarta atau America/Los_Angeles, teks hasil render awal akan berbeda.
Pola yang lebih aman:
// Lebih aman: tentukan locale dan timezone eksplisit
const formatter = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'UTC'
});
const label = formatter.format(new Date(order.createdAt));Jika memang UI harus mengikuti timezone pengguna, jangan render string final di SSR kecuali timezone pengguna sudah diketahui dan konsisten. Alternatifnya, render placeholder stabil lalu format di klien setelah mount.
Font dan metrik teks
Perbedaan font tidak selalu menghasilkan warning hydration, tetapi bisa memicu cabang layout berbeda jika aplikasi mengukur ukuran teks, lebar kontainer, atau jumlah baris saat render awal. Font fallback di Mac remote yang tidak sama dengan mesin lokal juga bisa mengubah tinggi elemen, wrapping, dan snapshot visual.
Masalah bertambah parah jika kode memutuskan isi DOM berdasarkan pengukuran layout atau media query pada render pertama.
Viewport dan media query
Jika komponen memilih markup berbeda untuk mobile vs desktop saat render pertama berdasarkan window.innerWidth, hasil SSR dan klien akan mudah mismatch. Server tidak memiliki viewport browser yang sesungguhnya.
// Buruk: nilai window hanya ada di klien dan memengaruhi markup awal
const isMobile = window.innerWidth < 768;
return isMobile ? <MobileNav /> : <DesktopNav />;Lebih aman memakai CSS responsif untuk perbedaan presentasi, atau melakukan percabangan setelah komponen ter-mount jika markup benar-benar harus berbeda.
Browser extension
Password manager, ad blocker, translation extension, atau accessibility extension dapat menyuntik atribut dan node tambahan ke DOM. Saat ini terjadi sebelum atau selama hydration, framework bisa melihat struktur DOM yang tidak lagi sama dengan HTML server.
Untuk QA remote, gunakan profil browser bersih atau mode automation yang extension-nya dinonaktifkan.
User agent dan capability detection
Kode yang bercabang berdasarkan user agent sering tampak aman di lokal, tetapi pecah ketika remote browser memakai versi Safari/Chrome berbeda, atau automation browser mengirim user agent khusus. Jika server merender berdasarkan header user agent, sedangkan klien mengambil keputusan berdasarkan API browser nyata, hasilnya bisa tidak sinkron.
Cookie, session, local storage, dan feature flag
SSR biasanya membaca cookie pada request. Setelah halaman dimuat, klien bisa membaca local storage, cookie tambahan, atau hasil fetch flag yang berbeda. Jika keputusan render awal bergantung pada sumber yang tidak identik, mismatch akan muncul.
// Buruk: server tidak punya akses ke localStorage,
// tetapi render awal klien bergantung padanya
const theme = localStorage.getItem('theme') || 'light';Solusi yang lebih stabil adalah menyatukan sumber kebenaran state awal ke payload SSR atau cookie request yang sama-sama terlihat oleh server dan klien.
Environment variable dan konfigurasi runtime
Dalam setup remote, build bisa dilakukan di mesin A, server berjalan di mesin B, dan browser automation di mesin C. Jika salah satu menggunakan base URL, default locale, feature flag, atau konfigurasi runtime yang berbeda, HTML hasil SSR dan asumsi klien bisa menyimpang.
Perhatikan juga perbedaan antara:
- variabel saat build
- variabel saat server runtime
- variabel di test runner atau remote browser harness
Cara mereproduksi bug secara sengaja
Bug hydration sulit diperbaiki jika tidak bisa direproduksi dengan stabil. Justru langkah terbaik adalah membuatnya sengaja gagal.
Skenario reproduksi minimal
- Jalankan server SSR dengan locale dan timezone tertentu, misalnya
UTC. - Buka halaman dari browser remote dengan timezone berbeda.
- Render teks yang diformat menggunakan API locale default.
- Bandingkan HTML server dan DOM setelah hydration.
// Contoh komponen non-deterministik
export function OrderTime({ createdAt }) {
return <time>{new Date(createdAt).toLocaleString()}</time>;
}Komponen di atas cukup untuk memunculkan drift bila environment berbeda.
Strategi reproduksi yang praktis
- Uji dengan dua timezone berbeda, misalnya UTC vs Asia/Jakarta.
- Uji dua locale, misalnya
en-USvsid-ID. - Jalankan browser dengan viewport desktop dan mobile yang konsisten.
- Gunakan profil browser kosong lalu bandingkan dengan profil harian yang penuh extension.
- Bekukan cookie dan feature flag pada state yang diketahui.
Checklist diagnosis hydration drift
Saat bug hanya muncul di mesin remote, cek daftar berikut secara sistematis.
1. Bandingkan HTML server dengan DOM setelah hydration
Lihat View Source atau response HTML mentah dari server, lalu bandingkan dengan DOM final di DevTools. Fokus pada node yang berubah tepat setelah JavaScript aktif.
2. Log fingerprint environment
Tambahkan logging ringan pada server dan klien untuk menangkap context yang memengaruhi render:
// Log yang berguna untuk diagnosis
{
locale: Intl.DateTimeFormat().resolvedOptions().locale,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
userAgent: navigator.userAgent,
viewport: { width: window.innerWidth, height: window.innerHeight },
cookiesPresent: document.cookie,
}Di server, log juga locale default, timezone proses, header penting, dan flag aktif.
3. Cari pola render non-deterministik
Tinjau komponen SSR untuk penggunaan:
new Date()saat renderMath.random()crypto.randomUUID()untuk key atau id markup awalwindow,document,navigator,screen,matchMedialocalStorageatausessionStorage- formatting locale default tanpa parameter eksplisit
- pengukuran layout saat render pertama
4. Validasi state awal yang dipakai server dan klien
Periksa apakah data SSR, cookie request, session user, dan feature flag yang dipakai server sama dengan yang diakses klien saat hydrate.
5. Nonaktifkan extension dan custom profile
Jika bug hilang saat browser memakai profil bersih, kemungkinan besar ada injeksi DOM atau modifikasi perilaku dari extension.
6. Cek perbedaan build dan runtime
Pastikan artifact build yang dipakai server remote sama dengan yang diuji, dan tidak ada variabel lingkungan yang berbeda antara proses build dan runtime.
Pola kode yang harus dihindari
Jangan hitung nilai dinamis saat render SSR tanpa kontrol
// Buruk
export default function Hero() {
const now = new Date().toISOString();
return <p>Loaded at {now}</p>;
}Setiap render server dan klien akan menghasilkan nilai berbeda. Jika perlu, kirim nilai itu dari server sebagai prop dan gunakan nilai yang sama saat hydrate.
Jangan gunakan random untuk markup awal
// Buruk
const id = Math.random().toString(36).slice(2);Gunakan id yang berasal dari data, atau mekanisme framework yang memang dirancang stabil untuk SSR.
Jangan bercabang langsung pada API browser di render awal
// Buruk
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;Jika butuh personalisasi seperti tema sistem, pertimbangkan:
- set state default yang stabil untuk SSR
- sinkronkan dengan cookie yang dibaca server
- atau update setelah mount
Jangan parse tanggal ambigu
String tanggal tanpa timezone eksplisit dapat diperlakukan berbeda antar runtime. Simpan dan kirim timestamp atau string ISO lengkap dengan offset/Z.
Contoh implementasi di Next.js, Nuxt, dan Inertia
Next.js: format tanggal secara deterministik
Prinsipnya sederhana: format di server dan klien dengan aturan yang sama, atau tunda formatting yang benar-benar bergantung pada browser hingga setelah mount.
// util/formatDate.ts
export function formatOrderDate(input: string) {
return new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'UTC',
}).format(new Date(input));
}// Komponen SSR-safe
import { formatOrderDate } from './util/formatDate';
export function OrderMeta({ createdAt }: { createdAt: string }) {
return <time dateTime={createdAt}>{formatOrderDate(createdAt)}</time>;
}Jika Anda memang ingin menampilkan waktu lokal pengguna, gunakan fallback SSR yang stabil:
import { useEffect, useState } from 'react';
export function LocalizedTime({ createdAt }: { createdAt: string }) {
const [text, setText] = useState('');
useEffect(() => {
setText(new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(createdAt)));
}, [createdAt]);
return <time dateTime={createdAt}>{text || '—'}</time>;
}Trade-off-nya: tidak ada mismatch, tetapi ada placeholder sesaat sebelum nilai lokal pengguna ditampilkan.
Nuxt: pisahkan logika browser-only
Di Nuxt, bug umum muncul ketika state awal bergantung pada API browser. Simpan render SSR tetap deterministik, lalu jalankan penyesuaian browser-only setelah mount.
<script setup>
import { ref, onMounted } from 'vue'
const isDark = ref(false)
onMounted(() => {
isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches
})
</script>
<template>
<div :data-theme="isDark ? 'dark' : 'light'">
...
</div>
</template>Jika tema memengaruhi markup besar, lebih baik sumber tema awal disimpan di cookie agar server dapat merender varian yang sama.
Inertia: pastikan shared props stabil
Pada Inertia, sumber mismatch sering berasal dari shared props yang berbeda antara request SSR dan state yang dibaca frontend saat boot. Misalnya locale, auth state, atau feature flags.
Pastikan semua input penting untuk render awal dikirim melalui props dari server, bukan dihitung ulang dari local storage atau runtime browser. Jika ada flag eksperimen, serialisasikan ke page props dan gunakan sebagai sumber kebenaran tunggal untuk initial render.
Strategi hardening untuk CI dan remote QA
1. Standarkan locale, timezone, dan viewport
Remote browser dan runner sebaiknya menggunakan konfigurasi yang dipatok. Bahkan bila produk harus mendukung banyak locale, baseline test tetap perlu environment deterministik.
- Tentukan timezone default untuk test snapshot.
- Tentukan locale default untuk render otomatis.
- Tentukan ukuran viewport tetap untuk tiap skenario.
- Matikan auto-zoom dan fitur aksesibilitas yang mengubah layout bila tidak sedang diuji.
2. Gunakan profil browser steril
Untuk QA remote, siapkan profil khusus tanpa extension, tanpa login personal, dan dengan cookie yang dibersihkan sebelum suite berjalan. Ini menghilangkan sumber drift yang bukan berasal dari aplikasi.
3. Bekukan feature flag dan eksperimen
Jangan biarkan test automation ikut bucket eksperimen acak. Gunakan cookie, header, atau stub service agar semua request memakai set flag yang tetap.
4. Samakan artifact build dan runtime config
Build sekali, deploy artifact yang sama, dan log checksum atau identitas build di server serta browser test. Hindari kondisi di mana browser remote membuka server dengan config runtime yang berbeda dari asumsi test.
5. Tambahkan deteksi hydration warning ke automation
Dalam test end-to-end, tangkap warning console terkait hydration dan fail-kan test jika muncul. Ini membantu menemukan drift lebih awal, bukan setelah menjadi bug visual yang sulit dilacak.
Prinsipnya bukan sekadar memeriksa bahwa elemen tampil, tetapi memastikan render awal konsisten.
6. Simpan environment fingerprint pada hasil test
Saat test gagal, simpan informasi seperti timezone, locale, viewport, user agent, daftar extension aktif, cookie penting, dan build id. Data ini sering lebih berharga daripada screenshot saja.
Pendekatan desain yang lebih tahan drift
Utamakan pure render untuk SSR
Komponen SSR idealnya hanya bergantung pada input eksplisit: props, data fetch, dan config yang diketahui server serta klien. Semakin sedikit ketergantungan pada environment lokal browser, semakin kecil risiko mismatch.
Gunakan enhancement setelah mount untuk hal yang benar-benar client-specific
Ada informasi yang memang milik klien, seperti preferensi sistem, ukuran viewport aktual, atau timezone user akhir. Jangan paksa SSR menebaknya jika hasilnya dapat berbeda. Render baseline stabil dulu, lalu tingkatkan setelah mount.
Pisahkan masalah visual dan masalah hydration
Font, rendering engine, dan anti-aliasing bisa mengubah screenshot tanpa menyebabkan mismatch HTML. Sebaliknya, hydration mismatch bisa terjadi walau visual tampak hampir sama. Bedakan keduanya agar diagnosis tepat.
Penutup
Remote Browser SSR sering memunculkan bug yang tampak acak, padahal penyebabnya sistematis: render awal tidak deterministik antar mesin. Saat browser remote berjalan di Mac yang locale, timezone, font, viewport, cookie, extension, user agent, feature flag, atau environment variable-nya berbeda, HTML server dan hydration klien tidak lagi berbicara bahasa yang sama.
Langkah paling efektif adalah membuat input render awal eksplisit dan konsisten, menunda logika browser-only sampai setelah mount, serta menstandarkan environment pada CI dan remote QA. Jika Anda memperlakukan hydration sebagai kontrak deterministik antara server dan browser, sebagian besar bug “hanya muncul di mesin remote” akan jauh lebih mudah dicegah dan dilacak.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!