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, atau videoHeight setelah 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 elemen video atau img langsung 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, atau error.
  • 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 window atau 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

  1. Bandingkan HTML SSR dan render awal client. Lihat apakah jumlah node, urutan list, teks waktu, atau atribut seperti src dan style berubah sebelum interaksi.
  2. Audit semua nilai nondeterministik. Cari penggunaan waktu kini, locale, random, ukuran layar, object URL, dan metadata media.
  3. Log state awal di server dan client. Pastikan duration, frames, transcript.status, dan source.kind benar-benar sama.
  4. Bekukan skeleton. Gunakan jumlah item tetap dan teks placeholder tetap.
  5. Nonaktifkan sementara enhancement client. Jika warning hilang, berarti mismatch berasal dari state atau efek pasca-mount yang terlalu dini.
  6. Periksa jalur file lokal. Pastikan tidak ada blob: URL atau API browser yang ikut pada SSR path.
  7. Periksa pembulatan durasi dan rasio. Misalnya 59.94 detik bisa dibulatkan berbeda jika logic server dan client tidak sama.
  8. 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.