Audit dependensi SSR perlu dilakukan ketika aplikasi React atau Next.js menampilkan HTML dari server lalu melakukan hydration di browser. Jika output awal di server berbeda dari hasil render pertama di client, React dapat memunculkan warning hydration mismatch, mengganti subtree DOM, atau memicu UI yang “berkedip” setelah mount.

Masalah ini sering bukan berasal dari komponen yang Anda tulis sendiri, melainkan dari dependensi: library yang membaca window saat import, menghasilkan nilai acak saat render, memakai locale yang berbeda, atau menginisialisasi state dari storage/cookie dengan jalur yang tidak konsisten antara server dan browser. Dalam konteks audit rantai pasok dependensi, fokusnya bukan hanya keamanan paket, tetapi juga runtime behavior yang aman untuk SSR dan deterministik saat hydration.

Gejala yang Perlu Dianggap Serius

Tidak semua hydration mismatch langsung memecahkan aplikasi, tetapi gejalanya sering menjadi indikator desain render yang tidak deterministik.

  • Warning hydration failed atau pesan bahwa teks/atribut DOM tidak cocok antara server dan client.
  • UI berubah setelah mount, misalnya tombol, tema, tanggal, atau daftar item bergeser setelah JavaScript aktif.
  • State awal berbeda, contohnya komponen server menampilkan logged out tetapi client langsung berubah ke logged in.
  • Event handler terasa tidak stabil karena React terpaksa mengganti subtree hasil server.
  • Snapshot visual berbeda antara mode SSR dan hasil pertama di browser.

Jika gejala ini muncul hanya di produksi, curigai faktor lingkungan seperti timezone server, locale default, cookie, flag eksperimen, atau bundling yang berbeda.

Pola Akar Masalah pada Dependensi SSR

1. Akses window, document, atau API browser saat import

Masalah paling umum adalah paket yang mengeksekusi kode browser di level modul. Walau komponen Anda hanya memakai library itu di client, efek samping saat import bisa terjadi lebih awal pada proses SSR.

// Buruk: modul dieksekusi saat import dan langsung membaca window
const width = window.innerWidth;

export function getInitialLayout() {
  return width > 768 ? 'desktop' : 'mobile';
}

Dalam SSR, pola ini bisa gagal total atau menghasilkan cabang render yang berbeda. Lebih aman menunda akses API browser ke fase client saja.

2. Nilai waktu, locale, dan random yang tidak deterministik

Render yang memakai Date.now(), new Date(), Math.random(), atau format locale tanpa input yang diserialisasi berisiko menghasilkan output berbeda antara server dan browser.

// Buruk untuk SSR: nilai bisa berbeda per render
export function PromoBadge() {
  const minute = new Date().getMinutes();
  return <span>Promo menit {minute}</span>;
}

Bahkan jika selisihnya hanya satu detik, text node sudah cukup untuk memicu mismatch. Hal serupa terjadi pada Intl bila locale, timezone, atau data format tidak dikendalikan dengan jelas.

3. Side effect saat import

Beberapa library menulis ke global state, menginisialisasi singleton, menyuntikkan style, atau mendaftarkan listener sejak file dimuat. Ini sulit dilacak karena komponen tampak “murni”, tetapi perilaku aktualnya bergantung pada urutan import dan lingkungan runtime.

Contoh tipikal:

  • Library analytics yang langsung mengakses document.
  • Editor rich text yang membuat node DOM pada inisialisasi.
  • Paket UI yang membaca media query saat modul dimuat.

4. Feature detection yang memengaruhi hasil render awal

Feature detection bukan masalah jika hanya digunakan setelah mount, tetapi menjadi masalah bila hasilnya menentukan markup pertama.

// Berisiko: hasil render awal tergantung API browser
const supportsTouch = typeof window !== 'undefined' && 'ontouchstart' in window;

export function Hint() {
  return <p>{supportsTouch ? 'Tap' : 'Click'} untuk lanjut</p>;
}

Di server, hasilnya cenderung false. Di browser sentuh, hasil client menjadi true. Teks awal langsung berbeda.

5. Pembacaan storage atau cookie yang tidak konsisten

State awal tema, preferensi bahasa, eksperimen, atau sesi sering berasal dari localStorage, sessionStorage, atau cookie. Jika server merender berdasarkan satu sumber data dan client membaca sumber lain saat render pertama, mismatch hampir pasti terjadi.

Contoh umum:

  • Server memakai cookie tema default light, client membaca localStorage yang berisi dark.
  • Server belum tahu status autentikasi terbaru, client langsung membaca token lokal dan mengubah UI.
  • Eksperimen A/B dipilih ulang di browser tanpa serialisasi hasil yang sama dari server.

6. Library yang tidak SSR-safe

Banyak paket frontend sebenarnya dibuat untuk lingkungan browser murni. Paket seperti charting, editor, map, drag-and-drop, animation, atau device fingerprinting sering mengasumsikan DOM selalu ada. Sebagian bisa dipakai dalam SSR dengan pembatasan, sebagian lain sebaiknya hanya dirender di client.

Dalam audit dependensi, pertanyaannya bukan sekadar “apakah paket populer”, tetapi:

  • Apakah paket mendokumentasikan dukungan SSR?
  • Apakah entry point modulnya memiliki side effect?
  • Apakah output render-nya deterministik?
  • Apakah ada strategi fallback untuk server?

Langkah Investigasi Saat Hydration Mismatch Terjadi

Mulai dari subtree yang berubah, bukan dari seluruh aplikasi

Warning React biasanya mengarah ke node atau komponen yang bermasalah. Fokuskan pencarian ke subtree itu lalu telusuri dependensi yang dipakai oleh komponen tersebut.

  1. Identifikasi komponen yang memunculkan mismatch.
  2. Periksa semua import di file itu dan utilitas yang dipanggil saat render.
  3. Cari penggunaan Date, Math.random, Intl, window, document, storage, cookie, atau media query.
  4. Nonaktifkan dependensi satu per satu untuk menemukan pemicu minimum.

Bandingkan output SSR dengan render pertama di client

Secara praktis, Anda ingin membuktikan apakah server dan browser menghasilkan markup awal yang sama. Ini bisa dilakukan dengan snapshot HTML hasil SSR lalu dibandingkan dengan hasil render awal client sebelum efek berjalan penuh.

Untuk komponen yang kritis, buat pengujian yang:

  • merender komponen ke string di lingkungan server,
  • merender komponen yang sama di lingkungan browser terkontrol,
  • membandingkan teks dan atribut penting,
  • memastikan input seperti locale, timezone, dan props identik.

Audit entry point modul pihak ketiga

Jangan hanya melihat dokumentasi. Buka file distribusi paket atau source-nya jika memungkinkan. Cari pola berikut:

  • akses global seperti window, document, navigator, localStorage,
  • eksekusi fungsi saat top-level module load,
  • inisialisasi singleton yang bergantung pada browser,
  • pemanggilan waktu atau random saat import.

Audit ini sejalan dengan pemikiran rantai pasok yang lebih luas: dependensi tidak hanya dinilai dari kerentanan keamanan, tetapi juga dari perilaku runtime yang dapat merusak konsistensi hasil build dan render.

Mitigasi yang Praktis dan Aman

1. Gunakan dynamic import untuk komponen browser-only

Jika sebuah library jelas tidak SSR-safe, jangan dipaksa ikut render di server. Pada Next.js, pendekatan umum adalah memuatnya secara dinamis di client.

import dynamic from 'next/dynamic';

const ClientOnlyChart = dynamic(() => import('./ClientOnlyChart'), {
  ssr: false,
});

export default function Dashboard() {
  return (
    <section>
      <h2>Statistik</h2>
      <ClientOnlyChart />
    </section>
  );
}

Mengapa ini bekerja: komponen tidak dirender di server, sehingga tidak ada risiko mismatch dari library yang bergantung pada DOM. Trade-off: konten itu tidak hadir pada HTML awal, sehingga SEO, performa konten awal, dan pengalaman tanpa JavaScript bisa menurun.

2. Tambahkan guard client-only, tetapi jangan mengubah markup awal secara liar

Guard seperti typeof window !== 'undefined' berguna, tetapi sering disalahgunakan di dalam render sehingga server dan client justru menghasilkan DOM yang berbeda.

import { useEffect, useState } from 'react';

export function ClientThemeLabel() {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    const saved = window.localStorage.getItem('theme');
    if (saved) setTheme(saved);
  }, []);

  return <span>Tema: {theme}</span>;
}

Pola di atas aman jika default state yang dipakai server juga light. Jika tidak, UI tetap akan berubah setelah mount, walau warning hydration bisa berkurang. Solusi yang lebih baik adalah menyelaraskan sumber data awal antara server dan client.

3. Serialisasi state awal secara deterministik

Untuk tema, locale, sesi ringan, atau flag eksperimen, kirimkan state awal dari server ke client dalam bentuk yang sama. Dengan begitu render pertama di kedua sisi memakai input identik.

// Contoh konsep: server menentukan initialTheme dari cookie/request
export default function Page({ initialTheme }) {
  return <App initialTheme={initialTheme} />;
}

function App({ initialTheme }) {
  const [theme] = useState(initialTheme);
  return <html data-theme={theme}></html>;
}

Mengapa ini bekerja: hydration tidak bergantung pada pembacaan storage atau kondisi browser yang hanya tersedia di client. Setelah aplikasi stabil, sinkronisasi lanjutan bisa dilakukan di efek jika diperlukan.

4. Kendalikan waktu, random, dan locale

Untuk nilai yang harus tampil di HTML awal, hindari menghitungnya langsung saat render kecuali inputnya sudah diserialisasi. Beberapa pendekatan yang aman:

  • Kirim timestamp dari server sebagai prop, lalu format dengan aturan yang sama.
  • Jangan gunakan Math.random() untuk ID atau urutan item saat render.
  • Tetapkan locale/timezone eksplisit bila formatting harus konsisten.
  • Jika konten memang harus bergantung pada browser, render placeholder netral lalu isi di client.

5. Bungkus akses browser dalam adaptor yang jelas

Daripada membiarkan banyak komponen membaca global browser langsung, buat lapisan adaptor seperti storage.ts, runtime.ts, atau env.ts. Ini memudahkan audit dan pengujian.

export function readThemeFromBrowser() {
  if (typeof window === 'undefined') return null;
  return window.localStorage.getItem('theme');
}

Dengan pola ini, pencarian penggunaan API browser menjadi lebih terpusat dan lebih mudah ditinjau sebelum rilis.

Contoh Audit Dependensi pada React/Next.js

Checklist cepat saat menilai paket baru

  1. Baca dokumentasi SSR. Jika tidak ada penjelasan SSR sama sekali, anggap perlu verifikasi manual.
  2. Periksa entry point. Cari side effect top-level dan akses API browser.
  3. Uji dalam halaman SSR sederhana. Render komponen paket itu sendiri tanpa logika aplikasi lain.
  4. Bandingkan server HTML dengan client render awal. Fokus pada teks, atribut, dan urutan node.
  5. Periksa fallback. Apakah paket menyediakan mode non-DOM, lazy mount, atau API yang bisa ditunda ke useEffect?
  6. Catat dampak SEO/performa jika akhirnya harus di-dynamic import tanpa SSR.

Sinyal bahaya saat review kode

  • Import library UI berat langsung di komponen yang dirender server.
  • Inisialisasi state dari localStorage di fungsi render.
  • Pemanggilan new Date() untuk teks yang tampil di HTML awal.
  • Conditional render berbasis window.matchMedia.
  • Pembuatan ID acak untuk atribut yang harus cocok antara server dan client.

Pengujian Snapshot SSR vs Client

Anda tidak perlu membuat sistem pengujian yang rumit untuk mulai mendapatkan manfaat. Untuk komponen yang sensitif terhadap SSR, buat pengujian sederhana yang memverifikasi bahwa output awal stabil.

import { renderToString } from 'react-dom/server';
import { render } from '@testing-library/react';
import { MyComponent } from './MyComponent';

test('SSR dan render awal client konsisten', () => {
  const props = {
    initialTheme: 'dark',
    nowIso: '2026-06-15T08:00:00.000Z',
  };

  const ssrHtml = renderToString(<MyComponent {...props} />);
  const { container } = render(<MyComponent {...props} />);

  expect(ssrHtml).toContain('dark');
  expect(container.textContent).toContain('dark');
});

Contoh di atas bukan verifikasi sempurna untuk seluruh DOM, tetapi cukup untuk menangkap sumber mismatch yang paling umum: input awal yang tidak deterministik. Jika Anda memakai pipeline E2E, tambahkan skenario yang membuka halaman SSR lalu memeriksa apakah ada warning hydration di console browser.

Catatan: Pengujian snapshot mentah bisa rapuh jika markup sering berubah. Prioritaskan assertion pada bagian yang benar-benar rawan mismatch: teks tanggal, tema, hasil feature detection, dan state yang berasal dari storage/cookie.

Checklist Review Dependensi Sebelum Rilis

  • Apakah semua dependensi yang dipakai pada jalur SSR telah ditandai SSR-safe, client-only, atau perlu adaptor?
  • Apakah ada import yang memicu side effect browser di level modul?
  • Apakah state awal untuk tema, sesi ringan, locale, dan eksperimen sudah berasal dari sumber deterministik yang sama?
  • Apakah penggunaan tanggal, waktu, random, dan formatting locale pada HTML awal sudah dikendalikan?
  • Apakah komponen browser-only sudah dipisah dengan dynamic import atau boundary client-only yang jelas?
  • Apakah ada pengujian yang membandingkan perilaku SSR dan client untuk komponen kritis?
  • Apakah warning hydration di console browser diperlakukan sebagai blocker atau minimal sebagai sinyal investigasi wajib?
  • Apakah review dependensi mencakup perilaku runtime, bukan hanya lisensi dan CVE?

Kesimpulan

Audit dependensi SSR adalah langkah praktis untuk mencegah hydration mismatch di produksi. Intinya sederhana: render awal harus deterministik, menggunakan input yang sama di server dan client, serta bebas dari asumsi bahwa API browser selalu tersedia.

Pada React dan Next.js, solusi yang paling sering efektif adalah memisahkan library browser-only dengan dynamic import, menambahkan guard client-only secara hati-hati, menyerialisasi state awal dari server, dan menguji konsistensi output SSR vs client. Jika Anda memasukkan pemeriksaan ini ke proses review dependensi sebelum rilis, banyak bug UI yang sulit direproduksi bisa dicegah lebih awal.