Hydration mismatch sering terjadi pada aplikasi Next.js atau React SSR yang merender komputasi simbolik dan ekspresi matematika menggunakan KaTeX. Masalah ini berakar dari cara browser memproses MathML sebagai foreign content (namespace http://www.w3.org/1998/Math/MathML) dan mendekode entitas HTML secara otomatis, sedangkan runtime Node.js menghasilkan string statis murni tanpa normalisasi pohon DOM.
Akar Masalah: Perbedaan Tokenisasi XML/MathML dan HTML Entity
Saat server mengeksekusi katex.renderToString(), output yang dikirimkan adalah representasi teks mentah. Kerusakan hidrasi (drift simbolik) dipicu oleh tiga faktor utama:
- Dekode Entitas Simbolik Komputasi: Node.js menghasilkan string entitas seperti
⁢(U+2062),⁡(U+2061), atau−. Saat string ini diterima peramban, HTML5 tree builder langsung mendekode entitas tersebut menjadi karakter Unicode mentah di dalam text node. Ketika algoritma rekonsiliasi React berjalan di klien, React membandingkan VDOM berbasis string dengan DOM peramban yang telah didekode, sehingga memicu peringatan ketidakcocokan atribut atau text content. - Perilaku Self-Closing Tag MathML: MathML mendukung tag berformat self-closing seperti
<mspace />atau<none />. Namun, jika KaTeX dipasang langsung ke VDOM React alih-alih melalui inner HTML injection, parser JSX merender tag XML tersebut sebagai tag HTML biasa tanpa namespace XML yang valid. - Normalisasi Spasi: Tag
<mtext>dan<mo>mempertahankan spasi berbeda di Node.js vs DOM browser yang terpengaruh kompresi whitespace bawaan browser.
Strategi Solusi: Normalisasi AST dan Determinisme Serialisasi
Langkah preventif pertama adalah memastikan string keluaran KaTeX telah deterministik dan dinormalisasi ke karakter Unicode UTF-8 sebelum dikirim melalui streaming SSR, bukan membiarkan peramban menangani resolusi entitas secara ambigu.
Alternatif lebih ringkas: gunakan flag output: 'htmlAndMathml' pada konfigurasi KaTeX dengan deklarasi eksplisit bahwa markup tersebut statis.
Implementasi Komponen: Isolasi Boundary dan Fallback Crawler
Komponen berikut mengisolasi subtree matematika dari siklus rekonsiliasi VDOM React klien menggunakan dangerouslySetInnerHTML dan suppressHydrationWarning, sekaligus menyediakan representasi teks mentah untuk crawler mesin pencari dan pembaca layar (screen reader) tanpa memicu Cumulative Layout Shift (CLS).
// components/MathRenderer.tsx
import React from 'react';
import katex from 'katex';
interface MathRendererProps {
math: string;
displayMode?: boolean;
fallbackText?: string;
}
export function MathRenderer({ math, displayMode = false, fallbackText }: MathRendererProps) {
// ponytail: renderToString synchronous; upgrade to worker pool if expressions exceed 1000/page.
const html = katex.renderToString(math, {
displayMode,
output: 'htmlAndMathml',
throwOnError: false,
strict: false,
});
return (
<span
className="katex-ssr-boundary"
style={{
display: displayMode ? 'block' : 'inline-block',
minHeight: displayMode ? '2.5rem' : '1.2rem', // Reduksi Layout Shift (CLS)
}}
>
<span
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: html }}
/>
{fallbackText && (
<span
aria-hidden="true"
style={{
position: 'absolute',
width: '1px',
height: '1px',
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
border: 0,
}}
>
{fallbackText}
</span>
)}
</span>
);
}
Dilewati: Sanitasi DOM via DOMPurify di sisi server. Tambahkan jika input matematika berasal dari pengguna publik tanpa sanitasi backend.
Verifikasi Tree DOM: Runnable Parity Check
Gunakan script assert native Node.js berikut untuk memverifikasi konsistensi serialisasi antara output KaTeX dengan deserialisasi struktur DOM.
// check-parity.mjs
import assert from 'node:assert/strict';
import katex from 'katex';
const rawExpression = 'f(x) = x^2 + 2x - 5';
// 1. Eksekusi render SSR
const ssrString = katex.renderToString(rawExpression, {
output: 'htmlAndMathml',
displayMode: false,
});
// 2. Simulasi deteksi entitas kritis
const problematicEntities = ['−', '⁢', '⁡'];
const hasAmbiguousEntity = problematicEntities.some(ent => ssrString.includes(ent));
// Validasi struktur container MathML
assert.ok(ssrString.includes('<math'), 'FAIL: MathML markup tidak ditemukan.');
assert.ok(ssrString.includes('<semantics>'), 'FAIL: Semantics node hilang.');
if (hasAmbiguousEntity) {
console.warn('WARN: Terdeteksi entitas bernama HTML. Pastikan suppressHydrationWarning terpasang.');
} else {
console.log('SUCCESS: Tokenisasi SSR lolos verifikasi parity.');
}
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!