SSR Video Preview tanpa hydration mismatch berarti HTML yang dirender di server harus semirip mungkin dengan hasil render awal di browser sebelum interaktivitas penuh aktif. Pada komponen preview video, masalah ini sering muncul karena UI bergantung pada metadata video, hasil sampling frame, transcript, object URL lokal, atau API browser yang baru tersedia setelah client mount.
Dalam konteks proyek seperti claude-real-video, sumber data preview bisa berasal dari pipeline frame deduplication, scene-aware extraction, transcript, dan input URL/file. Semua ini mudah memicu perubahan UI setelah hydration jika state awal tidak distabilkan. Solusi utamanya bukan “mematikan SSR”, tetapi memastikan markup awal stabil, menunda bagian yang nondeterministik, dan menyerialisasi state yang benar-benar diketahui saat server render.
Mengapa komponen preview video rentan hydration mismatch
Hydration mismatch terjadi ketika struktur atau isi DOM hasil SSR berbeda dengan hasil render pertama di client. Untuk komponen video preview, perbedaan kecil pun cukup untuk memicu warning, rerender, atau perilaku UI yang sulit dilacak.
Sumber mismatch yang paling umum
- Durasi dan metadata video belum siap saat SSR. Browser baru mengetahui
duration,videoWidth, atauvideoHeightsetelah metadata termuat. Jika server menebak nilainya atau client langsung menampilkan nilai aktual, markup awal akan berbeda. - Perbedaan hasil sampling frame. Jika thumbnail dihasilkan dari kalkulasi yang bergantung pada durasi aktual, floating point, atau hasil ekstraksi yang hanya diketahui setelah proses client-side, urutan atau jumlah frame dapat berubah.
- State awal transcript tidak konsisten. Server mungkin hanya punya status
pending, sementara client langsung mengisi potongan transcript dari cache lokal atau request yang selesai sangat cepat. - Object URL lokal. File input dari pengguna menghasilkan
blob:URL yang tidak mungkin diketahui server. Jika elemenvideoatauimglangsung bergantung pada object URL saat render awal, mismatch hampir pasti terjadi. - Format waktu dan aspek rasio. Perbedaan pembulatan durasi, timezone, locale, atau fallback rasio 16:9 versus rasio aktual video dapat mengubah teks dan layout.
- Akses API browser saat render. Penggunaan
window,document,URL.createObjectURL,ResizeObserver, atau pembacaan dimensi media pada fase render akan memecah determinisme SSR.
Prinsip desain: buat markup server stabil dan deterministik
Target utama SSR bukan menampilkan seluruh preview final sejak awal, tetapi mengirimkan initial UI yang valid, berguna, dan identik antara server dan render pertama client. Setelah hydration selesai, barulah bagian dinamis ditingkatkan secara bertahap.
1. Pisahkan state menjadi tiga lapisan
- State SSR-safe: data yang benar-benar diketahui di server, misalnya sumber input bertipe URL remote, job ID, status pipeline, thumbnail statis yang sudah diproses backend, atau transcript ringkas yang sudah tersimpan.
- State hydration-safe: placeholder deterministik seperti
duration: null,aspectRatio: "16/9",frames: [],transcriptStatus: "loading". - State client-only: object URL lokal, metadata aktual video lokal, progress pembacaan file, frame hasil ekstraksi browser, dan info berbasis API media.
Pemisahan ini membantu Anda memutuskan apa yang boleh ikut SSR, apa yang harus diserialisasi, dan apa yang harus ditunda sampai mount.
2. Server hanya merender fallback yang juga akan dirender client
Jika durasi belum diketahui, jangan render 00:37 di server hanya karena “perkiraan”. Render saja placeholder yang sama di client sebelum metadata siap, misalnya --:-- atau skeleton teks. Aturan ini sederhana, tetapi paling efektif mencegah mismatch.
3. Gunakan identitas data yang stabil
Untuk preview hasil pipeline, gunakan ID scene, ID asset, atau URL thumbnail hasil backend, bukan hasil kalkulasi ad hoc di browser. Misalnya, bila scene-aware extraction dijalankan di backend, kirim daftar scene yang sudah final ke client. Jangan biarkan server menampilkan 6 frame sementara client menghitung ulang menjadi 5 frame berdasarkan metadata lokal.
Arsitektur aman untuk video preview berbasis SSR
Pola yang aman secara umum adalah: SSR menghasilkan shell preview yang stabil, lalu client memperkaya detail yang memang hanya tersedia di browser.
Contoh model data awal
{
"source": {
"kind": "remote-url",
"value": "https://cdn.example.com/video.mp4"
},
"preview": {
"posterUrl": "https://cdn.example.com/thumbs/job-123/poster.jpg",
"aspectRatio": "16/9",
"duration": null,
"frames": [
{ "id": "scene-1", "url": "https://cdn.example.com/thumbs/job-123/s1.jpg" },
{ "id": "scene-2", "url": "https://cdn.example.com/thumbs/job-123/s2.jpg" }
]
},
"transcript": {
"status": "loading",
"segments": []
},
"job": {
"id": "job-123",
"status": "processing"
}
}State seperti ini aman untuk SSR karena semua nilainya stabil. Jika data belum ada, tetap eksplisit dengan null, array kosong, atau status loading yang deterministik.
Contoh struktur UI yang stabil
<section class="video-preview" data-hydrated="false">
<div class="player-shell" style="aspect-ratio:16/9">
<img src="/placeholder/poster.svg" alt="Preview video" />
</div>
<div class="meta">
<span class="duration">--:--</span>
<span class="source-label">Memuat metadata</span>
</div>
<ul class="frame-strip">
<li class="frame-skeleton"></li>
<li class="frame-skeleton"></li>
<li class="frame-skeleton"></li>
</ul>
<div class="transcript">
<p>Transcript sedang diproses...</p>
</div>
</section>Markup ini bisa dirender identik oleh server dan client. Setelah mount, Anda boleh mengganti placeholder dengan metadata nyata, frame final, atau transcript aktual.
Pola implementasi yang aman lintas framework
Walau detail API berbeda, pola berikut berlaku untuk Next.js, Nuxt, dan SvelteKit.
1. Stable server markup
Prinsipnya: render hanya data yang tersedia dan stabil di kedua sisi. Hindari percabangan render berdasarkan API browser pada fase render awal.
function getInitialPreviewState(input) {
return {
sourceKind: input.kind, // "remote-url" | "local-file"
canRenderPlayableVideo: input.kind === 'remote-url',
duration: null,
aspectRatio: '16/9',
transcriptStatus: 'loading',
transcriptSegments: [],
frames: [],
hydrated: false
}
}Untuk input local file, SSR tidak mungkin mengetahui object URL atau metadata media. Jadi state awal harus mengakui keterbatasan itu, bukan pura-pura merender player final.
2. Hydration gate
Hydration gate adalah mekanisme untuk menunda bagian UI tertentu sampai komponen benar-benar sudah ter-mount di client. Ini cocok untuk elemen yang memerlukan createObjectURL, pembacaan metadata, atau API observer.
let hydrated = false
onMount(() => {
hydrated = true
})
const showInteractiveVideo = hydrated && source.kind === 'local-file'Selama hydrated === false, render shell yang sama dengan SSR. Setelah mount, baru tampilkan subkomponen interaktif atau ganti sumber media.
3. Skeleton deterministik
Skeleton sebaiknya tidak bergantung pada perhitungan runtime yang berubah-ubah. Jangan menghitung jumlah thumbnail skeleton dari lebar container saat render awal, karena lebar belum tentu sama antara server dan client. Tentukan jumlah tetap, misalnya 3 atau 4 item.
<ul class="frame-strip" aria-busy="true">
<li><div class="skeleton frame"></div></li>
<li><div class="skeleton frame"></div></li>
<li><div class="skeleton frame"></div></li>
</ul>4. Serialisasi state awal dari server
Jika backend sudah memiliki hasil pipeline seperti transcript parsial atau daftar scene hasil deduplication, serialisasikan sebagai bagian dari payload SSR. Dengan begitu client tidak perlu menebak state awal berbeda.
Yang penting, bentuk data dan nilai default harus identik. Misalnya, jika transcript belum siap, kirim { status: "loading", segments: [] } dari server, dan gunakan struktur yang sama di client sebelum fetch tambahan selesai.
5. Isolasi komponen client-only bila memang perlu
Tidak semua bagian wajib ikut SSR. Jika ada subkomponen yang seluruh nilainya bergantung pada browser, lebih aman memisahkannya sebagai client-only island.
- Next.js: gunakan pemisahan komponen client untuk area yang butuh browser API, sambil mempertahankan parent shell tetap SSR.
- Nuxt: bungkus bagian yang murni client-side dengan mekanisme client-only yang tersedia, tetapi jangan jadikan seluruh halaman client-only jika shell preview masih bisa di-SSR.
- SvelteKit: gunakan guard mount atau pemisahan komponen agar render awal tetap deterministik.
Trade-off-nya, semakin banyak area client-only, semakin sedikit manfaat SSR untuk konten tersebut. Karena itu isolasi hanya bagian yang benar-benar tidak bisa stabil di server.
Kasus nyata: sumber mismatch pada pipeline claude-real-video
Frame deduplication dan scene-aware extraction
Pipeline seperti frame deduplication dan scene-aware extraction sebaiknya dianggap sebagai sumber kebenaran tunggal untuk thumbnail preview. Jika server sudah memiliki hasil final, client tidak perlu menghitung ulang frame list dari durasi video atau interval sampling lokal.
Pola aman:
- Backend menghasilkan daftar frame/scene dengan ID stabil.
- SSR menampilkan thumbnail yang sudah final, atau skeleton tetap bila job belum selesai.
- Client hanya meningkatkan interaksi, misalnya scrub preview atau highlight scene aktif.
Anti-pattern: server menampilkan 4 frame hasil backend, lalu client saat mount menghitung ulang dari video.duration dan mengganti menjadi 6 frame. Mismatch seperti ini sering tidak terlihat pada data tertentu, tetapi muncul sporadis pada video pendek, VFR, atau metadata yang terlambat.
Transcript yang masih diproses
Transcript sering tiba bertahap. Jika SSR merender “Transcript sedang diproses”, tetapi client langsung mengambil cache lokal berisi potongan teks dan menampilkannya pada render pertama, warning hydration bisa muncul.
Pola aman:
- Gunakan state awal tunggal:
loading,ready, atauerror. - Jika ingin memakai cache client, terapkan setelah hydration, bukan saat render pertama.
- Pastikan struktur DOM untuk mode loading berbeda secara terkendali hanya setelah mount.
Input URL vs file upload
Input URL remote relatif lebih mudah untuk SSR karena server bisa mengetahui sumbernya dan kadang bisa menyiapkan poster atau metadata. Sebaliknya, file lokal baru benar-benar bisa diproses di browser pengguna.
Rekomendasi:
- Untuk URL remote, SSR shell preview dengan poster/scene yang sudah ada dari backend bila tersedia.
- Untuk file lokal, SSR hanya placeholder netral: nama file bila sudah diketahui dari submit server, ikon file, ukuran jika tersedia dari payload form, dan status “metadata akan dibaca di browser”.
- Jangan render
<video src="blob:...">pada jalur SSR.
Contoh pola framework-agnostic
Model render dua fase
function renderPreview(state) {
return {
showPoster: true,
showVideoTag: state.hydrated && state.sourceKind === 'local-file',
durationText: state.duration ?? '--:--',
transcriptMode: state.transcriptStatus,
frames: state.frames.length ? state.frames : ['sk1', 'sk2', 'sk3']
}
}Pada SSR dan render pertama client, hasilnya tetap sama selama hydrated masih false. Setelah mount, state dapat berubah tanpa memicu mismatch karena hydration sudah selesai.
Pembacaan metadata setelah mount
onMount(async () => {
if (source.kind !== 'local-file') return
const objectUrl = URL.createObjectURL(source.file)
videoSrc = objectUrl
try {
const meta = await readVideoMetadata(objectUrl)
duration = meta.duration ?? null
aspectRatio = meta.width && meta.height
? `${meta.width}/${meta.height}`
: '16/9'
} finally {
hydrated = true
}
})Dalam implementasi nyata, set hydrated = true biasanya dilakukan segera saat mount, sedangkan metadata diisi menyusul. Yang penting, render awal sebelum data ini siap tetap sama dengan SSR.
Serialisasi state awal dari server ke client
const initialState = {
preview: serverData.preview ?? {
posterUrl: null,
duration: null,
aspectRatio: '16/9',
frames: []
},
transcript: serverData.transcript ?? {
status: 'loading',
segments: []
}
}Hindari membentuk default yang berbeda di server dan client. Satu fungsi pembentuk state awal yang dipakai bersama jauh lebih aman daripada dua implementasi terpisah.
Trade-off yang perlu dipahami
SSR penuh vs client-only parsial
- SSR penuh untuk shell preview memberi loading state yang cepat, struktur DOM stabil, dan SEO lebih baik untuk halaman yang relevan.
- Client-only untuk subkomponen media lebih aman untuk fitur yang sangat tergantung browser, tetapi mengurangi manfaat SSR pada bagian tersebut.
Pilihan terbaik biasanya hybrid: shell, status job, transcript placeholder, dan thumbnail final dari backend tetap SSR; sedangkan player interaktif lokal, object URL, dan metadata file dibatasi ke client.
Poster statis vs frame hasil browser
- Poster statis dari backend lebih stabil untuk SSR.
- Frame hasil browser cocok bila sumber hanya tersedia di client, tetapi harus ditampilkan setelah hydration.
Anti-pattern yang harus dihindari
- Menggunakan
Math.random(), waktu saat ini, atau ID acak saat render SSR. Ini langsung membuat output berbeda. - Membaca
windowatau metadata media di body render. Akses browser API harus dipindah ke hook mount/effect. - Menghitung jumlah skeleton dari ukuran viewport/container saat render awal. Ukuran aktual client tidak identik dengan server.
- Merender teks waktu dari locale berbeda. Format durasi dan tanggal sebaiknya distandarkan.
- Mengganti struktur DOM loading dan loaded terlalu cepat sebelum hydration selesai. Gunakan gate atau state awal yang konsisten.
- Menghitung ulang daftar frame di client padahal backend sudah memberi hasil final. Jadikan backend sebagai source of truth untuk hasil pipeline.
- Mencampur state upload lokal dengan state preview server tanpa pemisahan eksplisit. Bedakan jalur remote URL dan local file.
Checklist debugging hydration mismatch
- Bandingkan HTML SSR dan render awal client. Lihat apakah jumlah node, urutan list, teks waktu, atau atribut seperti
srcdanstyleberubah sebelum interaksi. - Audit semua nilai nondeterministik. Cari penggunaan waktu kini, locale, random, ukuran layar, object URL, dan metadata media.
- Log state awal di server dan client. Pastikan
duration,frames,transcript.status, dansource.kindbenar-benar sama. - Bekukan skeleton. Gunakan jumlah item tetap dan teks placeholder tetap.
- Nonaktifkan sementara enhancement client. Jika warning hilang, berarti mismatch berasal dari state atau efek pasca-mount yang terlalu dini.
- Periksa jalur file lokal. Pastikan tidak ada
blob:URL atau API browser yang ikut pada SSR path. - Periksa pembulatan durasi dan rasio. Misalnya
59.94detik bisa dibulatkan berbeda jika logic server dan client tidak sama. - Pastikan key list stabil. Untuk frame dan transcript segments, gunakan ID stabil dari backend, bukan index bila urutan bisa berubah.
Rekomendasi implementasi yang paling aman
Jika Anda membangun komponen preview video modern dengan SSR, pendekatan yang paling aman biasanya seperti ini:
- SSR hanya merender shell preview, poster fallback, status pipeline, dan transcript placeholder atau data final yang memang sudah ada.
- State awal diserialisasi dari server dengan bentuk yang eksplisit dan deterministik.
- Durasi, dimensi video, object URL, dan metadata file dibaca hanya setelah mount.
- Hasil frame deduplication dan scene-aware extraction berasal dari backend bila tersedia, lalu dipakai apa adanya oleh client.
- Bagian yang benar-benar browser-only diisolasi sebagai subkomponen client-only, bukan menjadikan seluruh halaman kehilangan SSR.
Dengan pola ini, Anda tetap mendapatkan manfaat SSR tanpa tersandung mismatch pada detail media yang memang baru valid di browser.
Intinya: pada SSR Video Preview tanpa hydration mismatch, jangan paksa server menebak data media yang belum diketahui. Render markup yang stabil, kirim state awal yang konsisten, lalu lakukan enhancement client secara bertahap dan terisolasi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!