Hydration mismatch terjadi ketika representasi Virtual DOM di sisi klien tidak identik dengan HTML statis yang dikirim oleh server. Masalah ini kerap muncul pada arsitektur Server-Side Rendering (SSR) modern yang mengonsumsi data streaming secara asinkron melalui konkurensi berbasis channel, seperti implementasi asyncio.Queue pada Python atau chan pada Go.
Ketika worker asinkron melakukan yield chunk data ke output stream tanpa sinkronisasi urutan, waktu tiba pesan (interleaved arrival) menjadi non-deterministik. Server merender elemen berdasarkan urutan kedatangan pesan, sementara rehydration di browser mengevaluasi snapshot data secara berbeda. Artikel ini membahas debugging hydration race condition akibat channel coroutine SSR serta implementasi solusinya.
Akar Masalah: Non-Deterministik Yield pada Channel SSR
Pola konkurensi multi-producer single-consumer (MPSC) mengumpulkan data dari berbagai coroutine secara paralel sebelum di-push ke HTTP chunked response stream. Jika masing-masing coroutine memiliki durasi IO yang variatif, urutan event yang masuk ke channel tidak terprediksi.
// Pseudocode Producer MPSC di Server
async def fetch_block_a(channel):
data = await db.query_a() # Butuh 35ms
await channel.put({"type": "A", "payload": data})
async def fetch_block_b(channel):
data = await api.call_b() # Butuh 15ms
await channel.put({"type": "B", "payload": data})
// Konsumen menghasilkan HTML langsung saat item tersedia
async def render_stream(channel, write_stream):
while True:
item = await channel.get()
write_stream.write(render_component(item))Pada server, Blok B masuk terlebih dahulu dan dirender sebelum Blok A. Namun, jika runtime klien membaca global state store dari payload JSON terpisah yang diproses dengan resolusi Promise berbeda, klien merender Blok A mendahului Blok B. Hasilnya adalah struktur DOM node yang tidak cocok.
Diagnosis dan Pola DOM Mismatch
Browser developer console menampilkan peringatan khas saat terjadi deviasi antara SSR DOM dan hydration snapshot:
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
Warning: Expected server HTML to contain a matching <div class="card-b"> in <main>.
Server: <main><div class="card-b">...</div><div class="card-a">...</div></main>
Client: <main><div class="card-a">...</div><div class="card-b">...</div></main>Verifikasi dilakukan dengan membandingkan respons raw payload network (sebelum JavaScript dieksekusi) terhadap state akhir dari framework hydration root.
Solusi 1: Buffer Deterministik Berbasis Sequence ID
Cara mengatasi kondisi balapan ini tanpa memblokir thread eksekusi adalah menyematkan urutan deterministik (sequence ID) pada tingkat produser coroutine. Stream consumer memegang buffer sementara dan hanya memproses chunk jika urutan ID sesuai.
import asyncio
from typing import Dict, Any
class SequencedBuffer:
def __init__(self, expected_start: int = 0):
self.expected_id = expected_start
self.buffer: Dict[int, Any] = {}
async def process_stream(self, rx_channel: asyncio.Queue, output_stream):
while True:
item = await rx_channel.get()
if item is None: # Sentinel penanda stream selesai
break
seq_id = item["seq_id"]
self.buffer[seq_id] = item["payload"]
# Flush buffer sesuai urutan terurut
while self.expected_id in self.buffer:
payload = self.buffer.pop(self.expected_id)
await output_stream.write(payload)
self.expected_id += 1
rx_channel.task_done()Solusi 2: Sinkronisasi Terurut Melalui Transfer State Terenkapsulasi
Jika server tetap menyajikan rendering streaming berbasis ketersediaan event terkini untuk meminimalkan Time to First Byte (TTFB), pastikan initial state pada dokumen HTML memuat index transfer state yang terurut secara tegas.
<!-- Serialized State di Akhir Dokumen SSR -->
<script id="__HYDRATION_STATE__" type="application/json">
[
{ "seq": 0, "id": "block-a", "data": { "title": "Widget A" } },
{ "seq": 1, "id": "block-b", "data": { "title": "Widget B" } }
]
</script>Di sisi klien, parser membaca data mentah melalui sorting sequence index sebelum menginstansiasi store frontend:
// Client Rehydration Entrypoint
function getHydrationState() {
const rawNode = document.getElementById('__HYDRATION_STATE__');
if (!rawNode || !rawNode.textContent) return [];
const rawData = JSON.parse(rawNode.textContent);
// Menjamin urutan komputasi state identik dengan tree SSR
return rawData.sort((a, b) => a.seq - b.seq);
}
const state = getHydrationState();
clientHydrate(App, state, document.getElementById('root'));Trade-off dan Karakteristik
- Sequenced Buffer di Server: Menjamin urutan output absolut, tetapi chunk rendering tertunda jika sequence item awal memiliki latensi IO tinggi (head-of-line blocking pada consumer buffer).
- Client-Side Re-ordering via Transfer State: Mempertahankan TTFB cepat dan streaming dinamis, namun meningkatkan ukuran inline script payload serta kompleksitas logic sinkronisasi klien.
Gunakan sequence-based queue jika layout hierarchy bersifat fixed (seperti rendering list terstruktur). Gunakan transfer state sorting jika komponen bersifat modular dan independen satu sama lain.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!