Mismatch hydration pada pratinjau PDF SSR biasanya terjadi ketika HTML yang dirender di server tidak sama dengan hasil render pertama di browser. Masalah ini sering muncul saat Anda menambahkan efek make-look-scanned pada preview PDF, karena pipeline render mengandalkan API browser, WASM, canvas, font, dan kadang sumber acak yang tidak deterministik.
Solusi utamanya bukan memaksa seluruh preview dirender identik di server, melainkan memisahkan bagian yang benar-benar browser-only, menjaga output SSR tetap stabil, dan memastikan komputasi visual yang sensitif terhadap environment dijalankan hanya setelah hydration. Jika tidak, gejalanya biasanya berupa warning hydration, UI berkedip, ukuran preview berubah, atau efek scan terlihat berbeda setelah halaman interaktif.
Kenapa pratinjau PDF “mirip hasil scan” rawan hydration mismatch
Fitur preview PDF dengan efek dokumen hasil scan umumnya melibatkan beberapa tahap: membaca file, merender halaman PDF ke canvas, menjalankan transformasi visual seperti noise, blur, desaturasi, rotasi kecil, atau kompresi, lalu menampilkan hasilnya sebagai gambar/canvas. Tahap-tahap ini tampak sederhana, tetapi banyak bergantung pada lingkungan runtime.
1. Akses window, document, dan API browser
Pada SSR, kode berjalan di server tanpa DOM browser. Jika komponen preview langsung memanggil window.devicePixelRatio, document.createElement('canvas'), URL.createObjectURL, atau FileReader saat render pertama, hasil server dan klien hampir pasti berbeda.
Bahkan jika aplikasi tidak crash, server bisa menghasilkan placeholder kosong sementara klien langsung menghasilkan elemen canvas atau image. Perbedaan struktur DOM ini cukup untuk memicu hydration mismatch.
2. Inisialisasi WASM tidak identik antara server dan klien
Inspirasi fitur seperti make-look-scanned sering memakai komputasi berbasis browser dan kadang modul WASM untuk pemrosesan gambar. WASM biasanya diinisialisasi secara asinkron, memerlukan fetch asset tambahan, atau bergantung pada API browser tertentu. Jika server merender sebelum modul siap, sementara klien sudah memuat hasil transformasi setelah hydration, markup awal tidak akan cocok.
Masalah lain: beberapa modul image processing tidak dirancang untuk SSR. Menjalankannya di server Node bisa gagal, atau justru menghasilkan output yang tidak sama dengan browser karena perbedaan engine, font rendering, atau implementasi canvas.
3. Ukuran canvas berubah karena layout klien berbeda
Pratinjau PDF hampir selalu bergantung pada ukuran kontainer. Di server, Anda tidak punya informasi layout aktual viewport pengguna. Jika lebar preview dihitung dari window.innerWidth, ResizeObserver, atau ukuran elemen setelah CSS diterapkan, maka server mungkin merender ukuran default, sedangkan klien menghitung ukuran baru setelah mount.
Akibatnya, atribut canvas seperti width dan height berubah saat hydration. Hasil visual pun ikut berubah: teks jadi lebih tajam atau lebih blur, crop bergeser, atau noise memiliki pola berbeda karena resolusinya berubah.
4. Noise atau random seed tidak deterministik
Efek hasil scan biasanya menggunakan noise, dust, sedikit skew, atau variasi kecerahan acak. Jika sumber acak memakai Math.random() saat render, server dan browser hampir pasti menghasilkan nilai berbeda. Ini bukan sekadar perbedaan visual kecil; jika output acak memengaruhi style inline, atribut gambar, atau urutan node, mismatch bisa terjadi.
Walaupun React atau Vue kadang masih bisa menoleransi perubahan tertentu setelah hydration, pengguna akan melihat preview berubah mendadak. Ini sering terasa seperti bug “gambar ganti sendiri beberapa milidetik setelah halaman tampil”.
5. Font dan text metrics tidak konsisten
Preview PDF atau overlay anotasi sering bergantung pada font tertentu. Di server, font mungkin belum tersedia atau fallback berbeda dengan browser pengguna. Perbedaan font memengaruhi text metrics, line break, positioning, dan hasil rasterisasi ke canvas.
Jika Anda merender placeholder berbasis teks atau metadata halaman yang bergantung pada ukuran font, struktur DOM bisa tetap sama tetapi dimensinya berbeda. Dalam skenario yang lebih sensitif, canvas hasil akhir juga berubah karena layout teks tidak identik.
6. State file upload hanya ada di browser
File yang diunggah pengguna biasanya berasal dari <input type="file"> atau drag-and-drop, yang state-nya tidak tersedia saat SSR. Jika komponen mengasumsikan file sudah ada saat render server, atau jika UI bergantung pada object URL yang hanya dapat dibuat di browser, server dan klien akan merender cabang UI yang berbeda.
Contoh umum: server menampilkan “tidak ada file”, lalu klien setelah hydration langsung menampilkan preview dari state lokal yang dipulihkan. Jika cabang UI ini tidak dirancang hati-hati, hydration warning muncul dan preview berkedip.
Pola aman: SSR stabil, preview diproses di browser
Untuk kasus ini, pola yang paling aman adalah:
- SSR hanya merender shell yang stabil: judul, area preview, skeleton, metadata ringan.
- Komputasi PDF/canvas/WASM hanya dijalankan di klien setelah komponen mount.
- Gunakan seed deterministik untuk noise/transformasi agar hasil preview tidak berubah tiap re-render.
- Kunci ukuran area preview sedini mungkin agar tidak bergeser setelah hydration.
Prinsipnya: server tidak perlu menghasilkan efek visual final jika environment-nya tidak bisa identik dengan browser. Yang penting, HTML awal tetap valid, stabil, dan tidak membuat hydration gagal.
Pola aman di Next.js
Dynamic import untuk komponen client-only
Jika komponen preview bergantung pada canvas, file input, object URL, atau WASM, lebih aman menjadikannya komponen client-only. Dengan begitu, server tidak mencoba merender bagian yang tidak deterministik.
import dynamic from 'next/dynamic';
const PdfScanPreview = dynamic(() => import('./PdfScanPreview'), {
ssr: false,
loading: () => <div className="preview-skeleton" aria-busy="true">Menyiapkan pratinjau...</div>
});
export default function Page() {
return (
<section>
<h2>Pratinjau PDF</h2>
<PdfScanPreview />
</section>
);
}Pendekatan ini efektif jika preview memang murni fitur klien. Trade-off-nya, isi preview tidak ikut dirender oleh server. Untuk SEO biasanya tidak masalah, karena pratinjau file pengguna memang bukan konten yang perlu diindeks.
Placeholder SSR yang stabil
Jangan biarkan server merender struktur yang berbeda jauh dari hasil klien. Placeholder sebaiknya memiliki:
- rasio atau tinggi area yang tetap,
- teks status yang sama sebelum dan sesudah mount jika file belum siap,
- tanpa elemen acak yang berubah setiap render.
export function PreviewShell() {
return (
<div
className="preview-shell"
style={{ width: '100%', aspectRatio: '210 / 297', background: '#f3f4f6' }}
>
<div className="preview-status">Pratinjau akan dibuat di browser</div>
</div>
);
}Dengan area yang sudah dikunci, Anda mengurangi layout shift dan mencegah canvas muncul dengan ukuran berbeda setelah hydration.
Pisahkan komputasi browser-only di useEffect
Inisialisasi PDF renderer, WASM, object URL, dan pembacaan file sebaiknya dilakukan setelah komponen mount, bukan di body render komponen.
'use client';
import { useEffect, useRef, useState } from 'react';
export default function PdfScanPreview({ file, seed }) {
const canvasRef = useRef(null);
const [ready, setReady] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function run() {
if (!file || !canvasRef.current) return;
try {
setReady(false);
// 1) baca file / render PDF
// 2) init WASM bila diperlukan
// 3) gambar ke canvas dengan seed deterministik
if (!cancelled) setReady(true);
} catch (e) {
if (!cancelled) setError('Gagal membuat pratinjau');
}
}
run();
return () => {
cancelled = true;
};
}, [file, seed]);
return (
<div className="preview-shell">
{!ready && !error && <div className="overlay">Memproses preview...</div>}
{error && <div className="overlay error">{error}</div>}
<canvas ref={canvasRef} width={1240} height={1754} />
</div>
);
}Yang penting di sini bukan angka resolusinya, melainkan fakta bahwa resolusi awal ditetapkan secara eksplisit dan tidak bergantung pada layout yang belum stabil.
Pola aman di Nuxt
Gunakan komponen client-only
Pada Nuxt, pola yang setara adalah membungkus komponen preview dengan <ClientOnly>. Ini mencegah SSR mencoba mengeksekusi logika browser-only.
<template>
<section>
<h2>Pratinjau PDF</h2>
<ClientOnly>
<PdfScanPreview :file="file" :seed="seed" />
<template #fallback>
<div class="preview-skeleton">Menyiapkan pratinjau...</div>
</template>
</ClientOnly>
</section>
</template>Jika Anda tetap ingin beberapa bagian dirender di server, batasi hanya pada shell atau metadata yang benar-benar stabil.
Jalankan inisialisasi hanya saat mounted
<script setup>
import { onMounted, ref, watch } from 'vue';
const props = defineProps({
file: Object,
seed: String
});
const canvasRef = ref(null);
const ready = ref(false);
const error = ref('');
async function renderPreview() {
if (!props.file || !canvasRef.value) return;
ready.value = false;
error.value = '';
try {
// init PDF/WASM/browser APIs di sini
ready.value = true;
} catch (e) {
error.value = 'Gagal membuat pratinjau';
}
}
onMounted(renderPreview);
watch(() => [props.file, props.seed], renderPreview);
</script>Hindari pemanggilan API browser di luar onMounted jika komponen tersebut masih mungkin disentuh oleh SSR.
Gunakan seed deterministik agar preview tidak berubah setelah hydration
Jika efek scan menambahkan noise atau variasi acak, gunakan seed yang stabil. Jangan panggil Math.random() langsung saat render awal. Seed bisa berasal dari kombinasi data yang konsisten, misalnya nama file, ukuran file, nomor halaman, dan versi preset efek.
function makeSeed(input) {
let h = 2166136261;
for (let i = 0; i < input.length; i++) {
h ^= input.charCodeAt(i);
h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
}
return String(h >>> 0);
}
const seed = makeSeed(`${file.name}:${file.size}:page-1:preset-v1`);Kemudian pakai seed tersebut untuk generator pseudo-random yang deterministik. Dengan pendekatan ini:
- preview halaman yang sama tetap konsisten,
- efek tidak berubah saat re-render komponen,
- hasil hydration tidak “meloncat” karena noise baru.
Jika Anda memang ingin tombol “acak ulang”, buat perubahan seed sebagai aksi eksplisit pengguna, bukan efek samping render.
Mengelola ukuran canvas dan font agar hasil tidak berubah
Kunci ukuran render internal
Gunakan satu sumber kebenaran untuk ukuran canvas internal. Hindari menghitungnya langsung dari viewport pada render pertama jika Anda ingin stabilitas tinggi. Pola umum yang lebih aman:
- tetapkan resolusi render internal tetap,
- skalakan tampilannya lewat CSS,
- ubah resolusi hanya setelah ukuran kontainer benar-benar diketahui dan perubahan dilakukan secara terkendali.
Dengan begitu, canvas tidak perlu dibongkar ulang hanya karena browser selesai menghitung layout.
Tunggu font siap sebelum rasterisasi teks
Jika preview melibatkan teks, watermark, atau anotasi yang dirender ke canvas, pastikan font sudah siap sebelum menggambar. Jika tidak, browser bisa merasterisasi dengan fallback font lalu menggambar ulang setelah font utama tersedia, sehingga preview berubah setelah hydration.
Secara praktis:
- pakai font yang benar-benar tersedia dan konsisten,
- jika perlu, tunda render canvas sampai font siap,
- hindari mengandalkan metrik teks server untuk hasil final browser.
Untuk shell SSR, lebih aman memakai placeholder netral daripada mencoba menebak hasil teks final.
State file upload: jangan anggap tersedia saat SSR
Pada aplikasi SSR, file upload adalah state klien. Perlakukan UI upload sebagai alur bertahap:
- SSR menampilkan shell kosong atau status “belum ada file”.
- Setelah mount, klien memulihkan state lokal jika memang ada.
- Baru setelah itu preview dibuat.
Jika Anda menyimpan informasi file di state global atau local storage, jangan langsung gunakan untuk mengubah struktur DOM pada render pertama tanpa guard. Pastikan transisi statusnya konsisten:
- SSR: belum ada preview.
- Hydration awal: masih placeholder yang sama.
- Setelah effect jalan: tampilkan status memproses.
- Sesudah selesai: ganti ke canvas/image final.
Alur ini mencegah React/Vue melihat node yang berbeda sebelum proses hydration selesai.
Strategi mencegah UI berkedip dan preview berubah setelah hydration
1. Pertahankan shell yang sama sampai hasil siap
Jangan merender konten setengah jadi yang lalu diganti total. Lebih baik tampilkan satu shell tetap dengan overlay status, lalu gambar hasil di dalam area yang sama.
2. Cache hasil preview berdasarkan file dan seed
Jika pengguna kembali ke halaman atau mengubah tab, Anda bisa menyimpan hasil render sementara di memori klien berdasarkan kunci seperti fileHash + page + seed + preset. Ini mengurangi render ulang yang tidak perlu dan menjaga output tetap konsisten.
3. Hindari rerender karena object identity
Perubahan referensi objek konfigurasi yang sebenarnya nilainya sama bisa memicu render ulang dan menghasilkan preview baru. Simpan konfigurasi efek dalam objek yang stabil atau memoized.
4. Pisahkan preview kasar dan efek akhir
Untuk pengalaman yang lebih halus, tampilkan dulu preview PDF dasar tanpa efek scan, lalu aplikasikan efek setelah pipeline siap. Tetapi lakukan di area yang sama dan dengan ukuran sama agar pengguna melihat progres, bukan pergantian layout.
Trade-off-nya: pengguna mungkin melihat dua tahap kualitas visual. Keuntungannya, waktu tampil pertama lebih cepat dan hydration lebih aman.
Checklist debugging mismatch hydration pada preview PDF
Gejala umum
- Warning hydration di console React/Vue.
- Preview kosong di server, lalu tiba-tiba muncul sesudah mount.
- Ukuran canvas berubah setelah halaman interaktif.
- Noise, blur, atau rotasi dokumen berbeda tiap refresh.
- Teks atau anotasi bergeser setelah font termuat.
- UI upload menampilkan status berbeda antara SSR dan klien.
Root cause yang paling sering
- Mengakses
window,document,FileReader,URL.createObjectURL, atau canvas saat SSR. - WASM diinisialisasi selama render, bukan setelah mount.
- Menggunakan
Math.random()untuk efek visual awal. - Menghitung ukuran canvas dari viewport atau elemen yang belum stabil.
- Mengandalkan font yang belum siap saat rasterisasi.
- Merender cabang UI berbeda karena state file hanya tersedia di browser.
Langkah pengecekan cepat
- Bandingkan HTML SSR dan render awal klien. Jika struktur node berbeda, cari percabangan berbasis environment.
- Cari akses browser API di level modul. Import yang langsung menyentuh
windowbisa gagal bahkan sebelum komponen mount. - Matikan efek acak sementara. Jika mismatch hilang, masalahnya ada pada seed atau noise.
- Kunci width/height canvas. Jika hasil stabil, penyebabnya kemungkinan layout-dependent sizing.
- Nonaktifkan sementara font kustom. Jika posisi teks membaik, fokus ke loading font.
- Render komponen sebagai client-only. Jika warning hilang, berarti memang ada bagian yang tidak aman untuk SSR.
Kapan perlu SSR penuh, kapan cukup client-only
Untuk preview PDF interaktif milik pengguna, client-only biasanya pilihan paling masuk akal. Nilai SEO dari hasil preview hampir tidak ada, sementara biaya menjaga render SSR identik dengan browser cukup tinggi.
SSR tetap berguna untuk:
- layout halaman secara umum,
- metadata dokumen yang sudah diketahui server,
- shell antarmuka yang cepat tampil.
Sebaliknya, bagian berikut sebaiknya dipindahkan ke klien:
- parsing file lokal,
- object URL,
- canvas rendering,
- efek visual berbasis WASM/browser,
- ukur ulang berbasis ukuran kontainer nyata.
Jika Anda memerlukan hasil yang benar-benar konsisten lintas pengguna dan runtime, pertimbangkan pipeline render terpusat di backend atau service image processing terpisah. Namun trade-off-nya adalah kompleksitas infrastruktur lebih tinggi, latensi tambahan, dan kebutuhan upload dokumen ke server.
Kesimpulan
Mengatasi mismatch hydration pada pratinjau PDF SSR berarti menerima bahwa efek dokumen “mirip hasil scan” bukan kandidat ideal untuk dirender identik di server dan klien. Penyebab utamanya biasanya kombinasi API browser, inisialisasi WASM, ukuran canvas yang berubah, sumber acak yang tidak deterministik, font, dan state file upload.
Pola yang paling aman adalah SSR untuk shell yang stabil, client-only untuk komputasi preview, ditambah seed deterministik, ukuran preview yang terkunci, dan inisialisasi setelah mount. Dengan pendekatan ini, Anda tidak hanya menghilangkan warning hydration, tetapi juga mencegah preview berkedip, berubah sendiri setelah hydration, atau menghasilkan output yang tidak konsisten.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!