Checklist audit hydration untuk SSR yang view-nya turun penting ketika halaman tampak normal pada first paint, tetapi berubah, meloncat, atau memunculkan error begitu React melakukan hydration. Dari sisi pembaca teknis, ini bukan sekadar bug visual: pengalaman baca terasa tidak stabil, navigasi membingungkan, dan kepercayaan terhadap isi halaman ikut turun.

Pada aplikasi Next.js/React, masalah ini biasanya muncul karena output server tidak identik dengan output awal di client. Penyebabnya sering praktis: nilai dari localStorage, waktu lokal browser, media query, A/B flag, data yang berubah di antara render server dan client, atau bahkan ekstensi browser yang mengubah DOM. Artikel ini merangkum audit yang bisa dipakai saat debugging, code review, dan verifikasi sebelum rilis.

Apa yang sebenarnya rusak saat hydration?

Pada SSR, server mengirim HTML yang sudah dirender. Lalu di browser, React melakukan hydrate: React menempelkan event handler dan menyambungkan state ke HTML yang sudah ada. Proses ini mengasumsikan bahwa markup awal dari server sama dengan markup awal yang dihitung di client.

Jika tidak sama, gejalanya bisa berupa:

  • Peringatan seperti text content did not match atau hydration failed.
  • Teks, ikon, atau tombol berubah sesaat setelah halaman tampil.
  • Layout meloncat karena komponen diganti setelah mount.
  • Event tidak terpasang sesuai ekspektasi pada subtree tertentu.
  • Komponen tertentu dirender ulang penuh di client, sehingga halaman terasa “kedip”.

Untuk situs artikel atau dokumentasi, mismatch hydration sering terasa lebih buruk daripada bug fungsional kecil. Pembaca datang untuk membaca cepat; jika UI berubah-ubah atau tombol berpindah, mereka cenderung pergi lebih cepat.

Checklist audit hydration untuk SSR yang view-nya turun

1) Mulai dari gejala yang bisa diamati

Sebelum membuka kode, catat gejalanya dengan jelas. Ini mempercepat pencarian sumber mismatch.

  • Apakah teks berubah setelah 1-2 detik?
  • Apakah hanya terjadi pada pengguna login, pengguna baru, atau perangkat mobile?
  • Apakah masalah hanya muncul di mode produksi?
  • Apakah masalah hilang saat JavaScript dimatikan?
  • Apakah subtree tertentu selalu berubah: header, CTA, daftar artikel, tema, tanggal, atau sidebar?

Jika halaman SSR “tampak benar” lalu view turun setelah hydration, besar kemungkinan ada logika client-only yang ikut menentukan tampilan awal.

2) Reproduksi masalah dengan cara yang konsisten

Hydration bug sering sulit ditangkap jika langkah reproduksinya tidak disiplin. Gunakan pola berikut:

  1. Uji di mode produksi lokal, bukan hanya development.
  2. Lakukan hard reload.
  3. Coba jendela incognito untuk menyingkirkan cache, cookie, dan ekstensi.
  4. Uji dengan ukuran viewport berbeda.
  5. Uji dengan timezone atau locale browser berbeda jika halaman menampilkan waktu/tanggal.
  6. Bandingkan hasil saat login vs logout jika ada personalisasi.

Kalau perlu, rekam video layar untuk menangkap momen sebelum dan sesudah hydration.

3) Bandingkan output server dan output client awal

Target auditnya sederhana: temukan nilai atau cabang render yang berbeda antara server dan client sebelum efek berjalan stabil.

Periksa bagian berikut:

  • Konten teks yang dibentuk dari waktu, angka, locale, atau data personal.
  • Percabangan render seperti isMobile ? A : B, isLoggedIn ? X : Y, atau theme === 'dark'.
  • List yang bergantung pada urutan data yang bisa berubah.
  • Komponen yang membaca window, document, matchMedia, atau localStorage langsung saat render.

Secara praktis, Anda ingin menjawab: nilai apa yang dipakai server, dan apakah client menghitung nilai awal yang sama?

Sumber mismatch yang paling sering

Data berubah antara server dan client

Jika data diambil saat SSR lalu diambil lagi di client dengan hasil berbeda, subtree bisa berubah tepat saat hydration atau sesudahnya. Ini umum pada konten yang cepat berubah: jumlah view, status stok, notifikasi, atau daftar rekomendasi.

Pola audit:

  • Pastikan data awal yang dipakai client berasal dari payload SSR yang sama.
  • Hindari refetch agresif tepat saat mount jika perubahan awal akan merusak pengalaman baca.
  • Jika data memang volatil, tampilkan nilai SSR dulu lalu refresh secara sadar dengan indikator yang tidak menggeser layout berlebihan.

State dari localStorage/sessionStorage

Server tidak bisa membaca localStorage. Jika render awal di client langsung membaca storage lalu menampilkan cabang UI berbeda, mismatch hampir pasti terjadi.

Before

function ReadingModeBanner() {
  const compact = localStorage.getItem('compact') === '1';
  return <div>Mode: {compact ? 'Ringkas' : 'Normal'}</div>;
}

Masalahnya: server tidak memiliki localStorage, sedangkan client punya. Render awal bisa berbeda.

After

import { useEffect, useState } from 'react';

function ReadingModeBanner() {
  const [compact, setCompact] = useState(false);

  useEffect(() => {
    setCompact(localStorage.getItem('compact') === '1');
  }, []);

  return <div>Mode: {compact ? 'Ringkas' : 'Normal'}</div>;
}

Pendekatan ini aman untuk hydration karena render awal server dan client sama-sama memakai nilai default yang konsisten. Trade-off-nya: nilai personal baru tampil setelah mount. Jika ini berdampak pada UX, pertimbangkan menyimpan preferensi di cookie yang juga bisa dibaca server.

Waktu, tanggal, timezone, dan locale

Format waktu adalah sumber mismatch klasik. Server dapat berada di timezone berbeda, locale berbeda, atau menghitung waktu beberapa milidetik lebih awal daripada browser.

Before

export function PublishedAt() {
  return <time>{new Date().toLocaleString()}</time>;
}

After

export function PublishedAt({ isoString }) {
  return <time dateTime={isoString}>{isoString}</time>;
}

Lalu jika ingin format lokal browser, lakukan setelah mount atau format di server dengan aturan yang konsisten. Intinya, jangan membuat nilai waktu dinamis saat render awal jika server dan client bisa menghasilkan output berbeda.

Alternatif praktis:

  • Kirim string yang sudah stabil dari server.
  • Gunakan placeholder netral dulu, lalu format lokal di client setelah mount.
  • Jika butuh SEO dan keterbacaan, render format server yang deterministik, bukan new Date() saat render.

Media query dan ukuran viewport

Server tidak tahu lebar viewport aktual browser. Jika komponen merender struktur yang berbeda berdasarkan matchMedia atau window.innerWidth, hasil awal bisa tidak cocok.

Before

function Toc() {
  const isMobile = window.matchMedia('(max-width: 768px)').matches;
  return isMobile ? <MobileToc /> : <DesktopToc />;
}

After

import { useEffect, useState } from 'react';

function Toc() {
  const [isMobile, setIsMobile] = useState(false);

  useEffect(() => {
    const media = window.matchMedia('(max-width: 768px)');
    const update = () => setIsMobile(media.matches);
    update();
    media.addEventListener?.('change', update);
    return () => media.removeEventListener?.('change', update);
  }, []);

  return <ResponsiveToc mobile={isMobile} />;
}

Lebih baik lagi jika perbedaan hanya berupa presentasi, serahkan ke CSS responsif. Gunakan JavaScript hanya bila benar-benar perlu mengubah perilaku.

A/B flag, feature flag, dan personalisasi

Flag yang dihitung berbeda di server dan client akan menyebabkan percabangan UI berbeda. Ini sering terjadi jika server membaca cookie tertentu, tetapi client menghitung ulang dari SDK, random assignment, atau hasil request lain.

Pola aman:

  • Tentukan variant di server.
  • Kirim variant itu ke client sebagai bagian dari props atau payload awal.
  • Gunakan nilai yang sama saat render pertama di client.
  • Hindari assignment acak seperti Math.random() saat render.

Untuk konten artikel, perubahan CTA, header, atau urutan blok rekomendasi karena flag yang tidak stabil dapat menurunkan fokus pembaca.

Ekstensi browser yang memodifikasi DOM

Tidak semua mismatch berasal dari aplikasi Anda. Password manager, translator, ad blocker, grammar checker, atau ekstensi aksesibilitas dapat menyisipkan atribut atau node ke DOM sebelum hydration selesai.

Ciri-ciri:

  • Masalah tidak bisa direproduksi di incognito tanpa ekstensi.
  • Node yang berubah tidak sesuai dengan kode aplikasi.
  • Peringatan hydration muncul di elemen input, form, atau teks yang sering disentuh ekstensi.

Langkah audit:

  • Reproduksi di profil browser bersih.
  • Bandingkan hasil dengan browser lain.
  • Fokus pada subtree yang memang rentan dimodifikasi ekstensi.

Ini penting agar tim tidak membuang waktu memperbaiki bug yang sebenarnya berasal dari lingkungan pengguna. Namun, tetap usahakan markup stabil dan tahan terhadap modifikasi kecil.

Langkah inspeksi yang praktis di Next.js/React

Isolasi komponen yang mencurigakan

Mulai dari komponen yang:

  • Membaca API browser saat render.
  • Menggunakan waktu saat ini, locale, atau random value.
  • Mengganti struktur DOM berdasarkan state yang baru tersedia di client.
  • Melakukan refetch langsung saat mount.

Jika perlu, komentari sementara subtree untuk mempersempit area mismatch.

Audit semua cabang render awal

Lihat fungsi render sebagai fungsi deterministik. Untuk setiap komponen SSR, tanyakan:

  • Apakah props awal identik antara server dan client?
  • Apakah ada state awal yang dihitung dari browser API?
  • Apakah ada logika yang bergantung pada waktu saat ini?
  • Apakah urutan array stabil?
  • Apakah key list konsisten?

Sering kali bug bukan pada data utama, melainkan pada satu kondisi kecil seperti label tombol atau blok info tambahan.

Bedakan mismatch render dengan update setelah mount

Tidak semua perubahan setelah halaman tampil adalah hydration error. Ada dua kategori:

  • Mismatch hydration: render awal server dan client tidak sama.
  • Update setelah mount: render awal sama, tetapi berubah setelah effect jalan.

Keduanya bisa sama-sama merusak UX, tetapi solusi teknisnya berbeda. Checklist ini fokus terutama pada kategori pertama, sambil tetap mengurangi update agresif setelah mount yang membuat view terasa turun.

Pola perbaikan yang aman

1) Gunakan nilai awal yang deterministik

Nilai render pertama harus bisa dihitung sama oleh server dan client. Jika tidak mungkin, pakai placeholder netral yang konsisten.

  • Baik: string, angka, atau status dari payload SSR.
  • Hati-hati: Date.now(), Math.random(), locale browser, viewport, storage.

2) Pindahkan logika client-only ke effect

Jika data hanya tersedia di browser, baca setelah mount. Ini mencegah mismatch, walau kadang menambah satu fase update visual. Untuk area penting seperti header artikel atau CTA utama, minimalkan perubahan layout saat update terjadi.

3) Gunakan cookie atau payload server untuk preferensi yang memengaruhi tampilan awal

Jika tema, mode baca, atau variant eksperimen harus akurat sejak first paint, simpan sumber kebenarannya di tempat yang juga bisa diakses server. Ini biasanya lebih baik daripada hanya mengandalkan localStorage.

4) Serahkan responsivitas ke CSS bila memungkinkan

Jika perbedaan mobile/desktop hanya visual, jangan pisahkan struktur render dengan JavaScript. CSS jauh lebih stabil untuk SSR.

5) Hindari nilai acak saat render

ID, variant, atau teks dinamis berbasis random harus dibuat dengan strategi yang stabil. Random saat render akan hampir selalu memicu ketidakkonsistenan.

Guardrail untuk review PR

Berikut daftar pertanyaan yang layak dijadikan checklist review PR pada proyek Next.js/React:

  • Apakah komponen SSR ini membaca window, document, localStorage, atau matchMedia saat render?
  • Apakah ada new Date(), Date.now(), atau format locale langsung di JSX render awal?
  • Apakah ada percabangan UI berdasarkan data yang hanya tersedia di client?
  • Apakah feature flag ditentukan satu kali secara konsisten dari server ke client?
  • Apakah data SSR direuse oleh client untuk render pertama, bukan dihitung ulang dengan hasil berbeda?
  • Apakah perubahan setelah mount akan menggeser layout penting untuk membaca?
  • Apakah perilaku tanpa ekstensi dan dengan browser bersih sudah diuji?

Checklist ini sederhana, tetapi efektif mencegah bug yang sulit dilacak setelah deploy.

Contoh audit singkat: tema dari localStorage vs cookie

Kasus umum: halaman SSR selalu tampil terang, lalu berubah ke gelap setelah hydration. Secara teknis mungkin “benar”, tetapi pengalaman baca awal terasa patah.

Before

function Page() {
  const theme = localStorage.getItem('theme') || 'light';
  return <main data-theme={theme}>...</main>;
}

Masalah: server tidak tahu tema, client tahu. Hasil awal berbeda.

After: prinsip lebih aman

function Page({ initialTheme }) {
  return <main data-theme={initialTheme}>...</main>;
}

Lalu initialTheme ditentukan di server dari sumber yang juga tersedia di request, misalnya cookie. Dengan begitu, first paint dan hydration memakai dasar yang sama. Jika sinkronisasi ke storage tetap dibutuhkan, lakukan setelah mount tanpa mengubah struktur inti halaman.

Daftar verifikasi sebelum rilis

  1. Buka halaman penting dalam mode produksi dan lakukan hard reload.
  2. Periksa console untuk warning hydration atau mismatch text.
  3. Uji halaman artikel, landing, dan header navigasi pada viewport kecil dan besar.
  4. Uji pada browser bersih/incognito untuk menyingkirkan pengaruh ekstensi.
  5. Pastikan tanggal, jam, locale, dan angka tidak berubah tak terduga setelah hydration.
  6. Pastikan tema, mode baca, atau personalisasi tidak mengubah first paint secara drastis.
  7. Pastikan feature flag dan eksperimen memakai variant yang sama di server dan client.
  8. Pastikan data SSR dipakai sebagai sumber render awal di client.
  9. Pastikan update setelah mount tidak menggeser CTA, heading, atau layout baca utama.
  10. Review komponen baru yang memakai browser API agar logikanya tidak berjalan saat render SSR.

Penutup

Hydration bug pada SSR sering terlihat kecil dari sisi implementasi, tetapi dampaknya langsung terasa pada kualitas pengalaman baca. Halaman yang tampak benar lalu berubah sesaat setelah interaktif membuat UI terasa tidak dapat dipercaya. Untuk konten teknis, itu cukup untuk membuat pembaca berhenti sebelum mereka benar-benar membaca.

Karena itu, audit hydration sebaiknya diperlakukan sebagai bagian dari quality gate, bukan sekadar debugging sesudah ada warning. Jika render awal server dan client dibuat deterministik, state personal ditangani dengan sumber data yang tepat, dan review PR memakai guardrail yang jelas, view yang “turun setelah hydration” bisa dicegah jauh sebelum sampai ke pengguna.