Implementasi middleware HTML minifier pada backend Go Fiber sering kali memicu error hydration mismatch pada aplikasi frontend berbasis React atau Vue yang menggunakan Server-Side Rendering (SSR). Masalah ini berakar pada bagaimana minifier memperlakukan karakter spasi, tabulasi, dan newline di antara elemen HTML.
Akar Masalah: Whitespace Text Node dan Siklus Hydration
Saat React atau Vue melakukan hidrasi (hydration), engine mencocokkan Virtual DOM (VDOM) yang dibangun di browser dengan representasi DOM aktual dari HTML yang dikirimkan oleh server. Node teks yang hanya berisi spasi kosong dihitung sebagai #text node independen dalam pohon DOM standar.
Ketika Go Fiber menerapkan minifikasi HTML secara agresif, spasi antar tag inline (seperti <span> atau <a>) sering kali dihapus seluruhnya. Akibatnya, struktur pohon DOM fisik tidak lagi memiliki #text node yang diharapkan oleh VDOM klien. Kondisi ini memicu peringatan runtime umum:
Error: Hydration failed because the initial UI does not match what was rendered on the server.
Perbandingan Diff DOM
Perbedaan struktural antara output template SSR dengan hasil minifikasi standar:
<!-- Output SSR Awal (Ekspektasi VDOM: 3 child nodes: span, #text, span) -->
<div id="root">
<span>Halo</span>
<span>Dunia</span>
</div>
<!-- Output Pasca-Minifikasi Agresif (Aktual: 2 child nodes: span, span) -->
<div id="root"><span>Halo</span><span>Dunia</span></div>Browser membuang #text node pemisah saat parsing HTML minified. Ketika JavaScript klien dieksekusi, rekonsiliasi gagal karena perbedaan jumlah node anak.
Konfigurasi Preserve Whitespace pada Middleware Fiber
Library minifikasi paling umum di ekosistem Go adalah github.com/tdewolff/minify/v2. Default konfigurasi minifier HTML pada package ini mengeliminasi whitespace redundant. Untuk mencegah kerusakan pohon DOM saat hidrasi, konfigurasi KeepWhitespace harus diaktifkan secara eksplisit.
Berikut implementasi middleware custom di Go Fiber:
package main
import (
"bytes"
"regexp"
"github.com/gofiber/fiber/v2"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/html"
)
func NewHTMLMinifier() fiber.Handler {
m := minify.New()
m.Add("text/html", &html.Minifier{
KeepWhitespace: true, // Kunci pencegahan hydration mismatch
KeepComments: false,
KeepEndTags: true,
KeepDocumentTags: true,
KeepQuotes: true,
})
return func(c *fiber.Ctx) error {
err := c.Next()
if err != nil {
return err
}
// Terapkan hanya pada response HTML
cType := string(c.Response().Header.ContentType())
if !bytes.HasPrefix([]byte(cType), []byte("text/html")) {
return nil
}
body := c.Response().Body()
minified, err := m.Bytes("text/html", body)
if err != nil {
// Fallback: kembalikan body asli jika minifikasi gagal
return nil
}
c.Response().SetBody(minified)
return nil
}
}Strategi Isolasi Hydration Payload
Selain tag teks inline, aplikasi SSR modern menyertakan payload hidrasi berupa script JSON seperti <script id="__NEXT_DATA__" type="application/json">. Minifikasi yang ceroboh berisiko merusak payload data ini jika regex atau parser salah membaca delimitasi script.
Gunakan langkah-langkah berikut untuk isolasi rute dan payload:
- Lewati minifikasi pada runtime dynamic injection: Pastikan script dengan
type="application/json"diabaikan oleh parser minifier dengan mendaftarkan parser khusus jika diperlukan. - Bypass middleware via path condition: Hindari minifikasi pada stream API internal atau rute SSR yang menggunakan streaming render (seperti React 18
renderToPipeableStream) karena memproses seluruh buffer di memori meniadakan keuntungan streaming.
Verifikasi Integritas Whitespace Menggunakan app.Test
Pastikan middleware tidak merusak spasi kritis dengan menulis integration test menggunakan utilitas pengujian internal Fiber app.Test.
package main
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestHTMLMinifierPreservesInlineWhitespace(t *testing.T) {
app := fiber.New()
app.Use(NewHTMLMinifier())
app.Get("/ssr", func(c *fiber.Ctx) error {
c.Set("Content-Type", "text/html; charset=utf-8")
// Format HTML yang merepresentasikan batas hidrasi komponen frontend
htmlPayload := `<div id="app"><span>First</span> <span>Second</span></div>`
return c.SendString(htmlPayload)
})
req := httptest.NewRequest(http.MethodGet, "/ssr", nil)
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("Request error: %v", err)
}
body, _ := io.ReadAll(resp.Body)
result := string(body)
// Validasi spasi kritis antara dua span tetap ada
expectedNode := `<span>First</span> <span>Second</span>`
if !strings.Contains(result, expectedNode) {
t.Errorf("Whitespace hilang pasca-minifikasi.\nExpected: %s\nActual: %s", expectedNode, result)
}
}Uji coba unit di atas mengonfirmasi bahwa spasi tunggal dipertahankan tanpa membiarkan newline atau indentation berlebih tetap ada dalam dokumen response.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!