Hydration mismatch terjadi ketika HTML yang di-render server berbeda dengan Virtual DOM yang dibangun browser pada render perdana. Pada arsitektur Server-Side Rendering (SSR)—baik menggunakan template engine backend maupun hybrid frontend (React/Vue/Inertia)—perbedaan zona waktu server dan browser pengguna sering menjadi sumber utama error Hydration failed because the initial UI does not match what was rendered on the server.
Akar Masalah: Desinkronisasi Timestamp Markup
Server backend secara standar berjalan dengan timezone UTC (time.UTC). Jika sebuah endpoint me-render data tanggal menggunakan fungsi lokal tanpa memperhitungkan posisi geografis klien, markup HTML yang dikirim ke browser berisi timestamp UTC:
<!-- Hasil render server (UTC) -->
<span id="login-time">2025-05-20 03:00:00 UTC</span>Ketika JavaScript di browser mengeksekusi proses hidrasi, fungsi format waktu seperti Intl.DateTimeFormat atau library tanggal mengevaluasi tanggal berdasarkan waktu lokal pengguna (misalnya Asia/Jakarta / UTC+7):
// Evaluasi client browser
const clientRender = new Date(isoString).toLocaleString(); // "2025-05-20 10:00:00"Perbedaan string teks antara node HTML server dan VDOM client memaksa runtime hydration membuang node terkait dan melakukan re-render penuh (DOM replacement), meningkatkan First Input Delay (FID) dan Cumulative Layout Shift (CLS).
Arsitektur Solusi: Sec-CH-Timezone dan Fallback Cookie
Untuk menyinkronkan output server dengan browser sebelum HTML dikirim, backend memerlukan informasi timezone pengguna pada fase HTTP request. Solusi paling bersih memanfaatkan HTTP Client Hints:
- Client Hint Header: Browser modern yang mendukung mengirim header
Sec-CH-Timezone(berisi IANA timezone sepertiAsia/Jakarta). Server wajib mendaftarkan header ini melalui responsAccept-CH: Sec-CH-Timezone. - Fallback Cookie: Untuk request pertama sebelum Client Hint terkirim, atau browser yang belum mengaktifkannya, simpan timezone dari skrip client (
Intl.DateTimeFormat().resolvedOptions().timeZone) ke dalam cookietz.
Implementasi Middleware Timezone di Go Fiber
Middleware berikut mengekstrak timezone pengguna secara thread-safe dari context, memvalidasi IANA timezone identifier, dan menyimpannya ke c.Locals.
package middleware
import (
"net/http"
"time"
"github.com/gofiber/fiber/v2"
)
const (
TimezoneContextKey = "user_timezone"
ClientHintHeader = "Sec-CH-Timezone"
FallbackCookie = "tz"
)
// TimezoneResolver memeriksa Client Hint lalu fallback ke Cookie
func TimezoneResolver(defaultLoc *time.Location) fiber.Handler {
return func(c *fiber.Ctx) error {
// Instruksikan browser untuk mengirimkan Client Hint pada request berikutnya
c.Set("Accept-CH", ClientHintHeader)
c.Vary(ClientHintHeader)
tzName := c.Get(ClientHintHeader)
if tzName == "" {
tzName = c.Cookies(FallbackCookie)
}
loc := defaultLoc
if tzName != "" {
if loadedLoc, err := time.LoadLocation(tzName); err == nil {
loc = loadedLoc
}
}
// ponytail: simpan pointer time.Location langsung di c.Locals, upgrade ke struct wrapper jika butuh locale/format tambahan
c.Locals(TimezoneContextKey, loc)
return c.Next()
}
}Passing Timezone ke Template Engine Fiber
Go Fiber menyediakan engine view seperti HTML (Go stdlib template). Integrasikan timezone resolver ke fungsi format tanggal global agar markup yang di-render backend menghasilkan format yang identik dengan browser.
package main
import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
"your-module/middleware"
)
func setupApp() *fiber.App {
engine := html.New("./views", ".html")
// Helper template untuk format waktu berbasis location context
engine.AddFunc("formatDate", func(t time.Time, loc *time.Location, layout string) string {
if loc == nil {
loc = time.UTC
}
return t.In(loc).Format(layout)
})
app := fiber.New(fiber.Config{
Views: engine,
})
app.Use(middleware.TimezoneResolver(time.UTC))
app.Get("/dashboard", func(c *fiber.Ctx) error {
userLoc, ok := c.Locals(middleware.TimezoneContextKey).(*time.Location)
if !ok {
userLoc = time.UTC
}
// Event waktu tetap (misal disimpan dalam format UTC di database)
eventTime := time.Date(2025, 5, 20, 10, 0, 0, 0, time.UTC)
return c.Render("index", fiber.Map{
"EventTime": eventTime,
"UserLoc": userLoc,
})
})
return app
}Contoh berkas views/index.html:
<!DOCTYPE html>
<html>
<body>
<div id="app">
<span class="timestamp">{{ formatDate .EventTime .UserLoc "2006-01-02 15:04:05 MST" }}</span>
</div>
</body>
</html>Verifikasi Menggunakan app.Test
Pengujian unit dapat memverifikasi bahwa respons markup berubah secara presisi mengikuti header Sec-CH-Timezone atau cookie, memastikan tidak ada desinkronisasi sebelum dikirim ke client.
package main
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestTimezoneHydrationRender(t *testing.T) {
app := setupApp()
tests := []struct {
name string
headerVal string
cookieVal string
expectedText string
}{
{
name: "Default ke UTC jika hint kosong",
headerVal: "",
cookieVal: "",
expectedText: "2025-05-20 10:00:00 UTC",
},
{
name: "Resolusi via Sec-CH-Timezone (WIB)",
headerVal: "Asia/Jakarta",
cookieVal: "",
expectedText: "2025-05-20 17:00:00 WIB",
},
{
name: "Fallback ke cookie tz jika header tidak ada",
headerVal: "",
cookieVal: "America/New_York",
expectedText: "2025-05-20 06:00:00 EDT",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/dashboard", nil)
if tc.headerVal != "" {
req.Header.Set("Sec-CH-Timezone", tc.headerVal)
}
if tc.cookieVal != "" {
req.AddCookie(&http.Cookie{Name: "tz", Value: tc.cookieVal})
}
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("Request gagal: %v", err)
}
body, _ := io.ReadAll(resp.Body)
bodyStr := string(body)
if !strings.Contains(bodyStr, tc.expectedText) {
t.Errorf("Ekspektasi render memuat %q, didapat: %s", tc.expectedText, bodyStr)
}
})
}
}Pertimbangan Edge Cases dan Caching
- Initial Request (First Load): Pada request pertama (cold visit), browser belum membaca response header
Accept-CH. Klien dapat menginjeksi cookietzmenggunakan snippet JavaScript kecil di<head>sebelum routing framework dijalankan, atau server fallback ke format netral ISO-8601 UTC dengan tagsuppressHydrationWarningjika menggunakan React. - Reverse Proxy Caching: Header respons
Vary: Sec-CH-Timezonewajib dikirim. Tanpa header ini, CDN (Cloudflare/Fastly) akan meng-cache HTML dengan timezone user A dan menyajikannya ke user B di belahan dunia berbeda. - Invalid Timezone String: Gunakan pengecekan error dari
time.LoadLocation. Jika klien mengirim nilai manipulatif (misalSec-CH-Timezone: Invalid/Location), sistem harus mengabaikan dan menggunakan default UTC secara graceful tanpa panic.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!