Anatomi Hydration Mismatch dan Silent Dead Click
Pada arsitektur Server-Side Rendering (SSR) seperti Next.js, Nuxt, atau SvelteKit, server mengirimkan dokumen HTML statis ke browser untuk mempercepat First Contentful Paint (FCP). Setelah berkas JavaScript dimuat di sisi klien, proses hydration berjalan. Proses ini mencocokkan Virtual DOM klien dengan DOM nyata dari server guna menempelkan (attach) interaktivitas dan event listener.
Ketika struktur DOM yang di-render klien berbeda dari HTML server, terjadi hydration mismatch. Dampak paling berbahaya dari anomali ini bukan layar putih atau hard crash, melainkan silent dead click. Elemen tombol atau form terlihat sempurna secara visual, namun tidak merespons klik pengguna sama sekali.
Kondisi ini terjadi karena reconciler (misalnya pada React 18+) mendeteksi ketidaksesuaian indeks node. Framework mungkin mengabaikan pemasangan event listener pada node yang dianggap korup, mencoba memulihkan sub-tree dengan cara menimpa node tanpa registrasi ulang, atau menempelkan handler ke elemen tetangga yang salah. Pengguna mengira aplikasi macet, padahal UI sekadar kehilangan jembatan JavaScript-nya.
Mengapa Test Runner Konvensional Kerap Lolos
Pengujian End-to-End (E2E) standar sering kali melewatkan silent dead click karena alasan berikut:
- Assert visual semata: Perintah
expect(locator).toBeVisible()hanya mengecek status visibilitas CSS dan keberadaan node pada DOM tree, bukan kesiapan event listener. - Toleransi Synthetic Click: Beberapa runner menyuntikkan event langsung via JavaScript jika elemen ditemukan, namun bila verifikasi akhir bergantung pada status internal aplikasi yang tidak terpicu, tes sering kali hanya dianggap flaky alih-alih diidentifikasi sebagai bug hidrasi.
- Hydration warning diabaikan: Log peringatan hidrasi hanya dicetak di
console.erroratauconsole.warnbrowser tanpa memutus eksekusi tes kecuali dikonfigurasi secara eksplisit.
Deteksi Otomatis Menggunakan Playwright dan CDP
Cara paling andal untuk mendeteksi silent dead click sebelum ke produksi adalah mengombinasikan penangkapan log konsol dengan inspeksi event listener langsung menggunakan Chrome DevTools Protocol (CDP).
CDP menyediakan domain DOMDebugger.getEventListeners yang memungkinkan test runner memeriksa apakah suatu node DOM benar-benar memiliki fungsi listener aktif pada runtime browser.
Contoh Implementasi Test Agent
Berikut skrip uji menggunakan Playwright untuk memvalidasi ketiadaan error hidrasi sekaligus memverifikasi keberadaan listener click pada elemen kritis:
import { test, expect } from '@playwright/test';
test('Verifikasi interaktivitas tombol dan ketiadaan hydration error', async ({ page }) => {
const hydrationErrors = [];
// 1. Tangkap seluruh warning dan error hidrasi dari konsol
page.on('console', (msg) => {
const text = msg.text();
if (
msg.type() === 'error' || msg.type() === 'warning'
) {
if (
text.includes('Hydration failed') ||
text.includes('did not match') ||
text.includes('server did not match')
) {
hydrationErrors.push(text);
}
}
});
await page.goto('https://app.internal/checkout', { waitUntil: 'networkidle' });
// 2. Buka sesi CDP untuk inspeksi low-level DOM
const client = await page.context().newCDPSession(page);
const buttonLocator = page.locator('#btn-submit-order');
await expect(buttonLocator).toBeVisible();
// Ambil remote object ID dari elemen DOM target
const elementHandle = await buttonLocator.elementHandle();
const { objectId } = await client.send('DOM.resolveNode', {
backendNodeId: (await client.send('DOM.describeNode', {
nodeId: (await client.send('DOM.requestNode', {
objectId: elementHandle._remoteObject.objectId,
})).nodeId,
})).node.backendNodeId,
});
// 3. Periksa event listener via DOMDebugger
const { listeners } = await client.send('DOMDebugger.getEventListeners', {
objectId,
});
const hasClickListener = listeners.some((l) => l.type === 'click');
// 4. Assert ketiadaan log error dan keberadaan listener
expect(hydrationErrors, `Ditemukan error hidrasi: ${hydrationErrors.join(' | ')}`).toHaveLength(0);
expect(hasClickListener, 'Listener click tidak terpasang pada tombol submit').toBeTruthy();
// Uji fungsionalitas aktual
await buttonLocator.click();
await expect(page.locator('#order-success-modal')).toBeVisible();
});
Pendekatan di atas memastikan dua hal: pipeline pengujian langsung gagal jika terjadi perbedaan struktur DOM server-klien, dan elemen kritis terbukti memiliki handler sebelum interaksi dijalankan.
Pola Perbaikan Frontend: Menjaga Determinisme Rendering
Penyebab utama hydration mismatch adalah ketidaksamaan eksekusi logika antara runtime Node.js di server dan V8 di browser. Terapkan pola-pola berikut untuk mencegah anomali tersebut.
1. Hindari Akses API Browser di Luar Siklus Klien
Mengakses objek seperti window, localStorage, atau navigator langsung saat instansiasi komponen menghasilkan output DOM server kosong atau berbeda dari klien.
Gunakan siklus hidup yang hanya dieksekusi di klien (seperti useEffect pada React) untuk membaca nilai dinamis tersebut:
// SALAH: Render berbeda langsung saat inisialisasi
function AuthStatus() {
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
return <div>{token ? <DashboardLink /> : <LoginForm />}</div>;
}
// BENAR: Two-pass rendering deterministik
import { useState, useEffect } from 'react';
function AuthStatus() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
if (localStorage.getItem('token')) {
setIsAuthenticated(true);
}
}, []);
// Server dan first client render identik
if (!mounted) {
return <div className="skeleton-placeholder" />;
}
return <div>{isAuthenticated ? <DashboardLink /> : <LoginForm />}</div>;
}
2. Standarisasi Format Tanggal dan Zona Waktu
Server yang berjalan dalam zona waktu UTC akan merender representasi tanggal berbeda dengan browser pengguna yang berada di WIB (UTC+7). Lakukan formatting tanggal hanya setelah komponen terpasang di klien, atau gunakan nilai zona waktu eksplisit yang dioper dari konfigurasi server ke klien.
3. Bersihkan Manipulasi DOM oleh Pihak Ketiga
Ekstensi browser (seperti pengelola kata sandi atau penerjemah otomatis) sering menyuntikkan elemen atau atribut sebelum skrip hidrasi selesai dieksekusi. Jika elemen tertentu memang memiliki konten yang tidak dapat diprediksi dari server (misalnya badge dinamis iklan), batasi dampaknya menggunakan properti framework:
<!-- React: abaikan perbedaan pada atribut/text spesifik -->
<span suppressHydrationWarning>
{new Date().toLocaleTimeString()}
</span>
Catatan: Gunakan suppressHydrationWarning hanya untuk text-node atau atribut isolatif. Jangan gunakan atribut ini pada level root container karena hanya akan menyembunyikan silent dead click tanpa memperbaiki reconciler yang rusak.Integrasi ke CI/CD Pipeline
Untuk mencegah bug masuk ke staging atau produksi, standarkan aturan pengujian berikut pada pipeline integrasi:
- Jadikan Hydration Warning Fatal: Konfigurasikan runner tes unit/komponen (seperti Vitest atau Jest) dan E2E (Playwright/Cypress) untuk melempar error bila terdapat panggilan
console.erroryang mengandung kata kunci hidrasi. - Validasi Halaman SSR Kritis: Fokuskan pengujian CDP listener pada halaman dengan konversi tinggi (alur autentikasi, checkout, pembayaran).
- Audit HTML Hasil Build: Jalankan verifikasi statis terhadap markup hasil SSR sebelum dideploy guna memastikan tidak ada tag HTML ilegal (seperti
<p>di dalam<p>atau<div>di dalam<span>) yang memicu perbaikan struktur otomatis oleh parser browser.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!