Fitur deferred props pada Inertia v2 (Inertia::defer()) memungkinkan server menunda kalkulasi data lambat hingga initial page payload selesai terkirim ke klien. Namun, saat digabungkan dengan Server-Side Rendering (SSR), deferred props kerap memicu dua masalah utama: hydration mismatch pada konsol browser dan skeleton flash yang merusak metrik Cumulative Layout Shift (CLS).

Masalah ini berakar dari ketidaksinkronan pohon DOM antara server markup awal dengan hasil rehidrasi client-side, terutama ketika komponen fallback mengembalikan struktur node yang tidak identik secara semantik.

Akar Masalah Hydration Mismatch pada Deferred Props

Saat request SSR diproses di backend (misal: Laravel), closure di dalam Inertia::defer() sengaja diabaikan. Node SSR process merender komponen dengan prop bernilai undefined. Komponen frontend membungkus area tersebut dengan komponen <Deferred> dan menampilkan slot #fallback.

Hydration mismatch terjadi ketika:

  • Struktur Tag Berbeda: Fallback merender tag pembungkus yang berbeda dengan komponen aktual, misalnya <div> untuk skeleton sementara komponen riil merender <section> atau <table>.
  • Whitespace dan Text Node Mismatch: Server merender fallback kosong atau placeholder string, sementara klien langsung menerima partial update sebelum fase hidrasi selesai, menyebabkan engine virtual DOM (Vue atau React) gagal mencocokkan DOM tree.
  • Client-Only State Injection: Logika render fallback menggunakan kondisi dinamis browser seperti pengecekan window atau timestamp lokal yang menghasilkan atribut berbeda antara SSR dan klien.

Strategi Mengatasi Skeleton Flash dan Degradasi CLS

Skeleton flash terjadi saat deferred prop selesai di-fetch dalam waktu sangat singkat (di bawah 100ms). Komponen fallback muncul sekejap lalu digantikan oleh data aktual, memicu flicker visual yang agresif. Jika dimensi skeleton tidak seimbang dengan konten final, browser terpaksa menghitung ulang geometri halaman (layout shift).

1. Isolasi Dimensi dengan CSS Aspect-Ratio dan Containment

Jangan biarkan skeleton menentukan ukuran wadah secara elastis tanpa batas. Bungkus komponen deferred dengan container yang memiliki reservasi tinggi minimum atau aspect-ratio tetap menggunakan properti CSS modern.

/* Mencegah pergeseran tata letak saat skeleton berganti ke konten aktual */
.deferred-card-wrapper {
    contain-intrinsic-size: 0 320px;
    content-visibility: auto;
    min-height: 320px;
}

2. Penyelarasan Semantik Komponen Fallback

Fallback skeleton harus merefleksikan hierarki node DOM target secara persis. Jika target adalah elemen list <ul> > <li>, fallback tidak boleh menggunakan kombinasi <div> > <span>.

Implementasi Kode: Backend Laravel dan Frontend Vue 3

Berikut pola konfigurasi backend menggunakan controller Laravel dan konsumsi data di Vue 3 dengan komponen pembungkus resmi Inertia v2.

Backend: Defer Prop di Controller

namespace App\Http\Controllers;

use App\Models\Metric;
use Inertia\Inertia;
use Inertia\Response;

class DashboardController extends Controller
{
    public function __invoke(): Response
    {
        return Inertia::render('Dashboard/Index', [
            'fastStats' => [
                'activeUsers' => 1250,
            ],
            // Kalkulasi berat ditunda dari initial payload SSR
            'revenueMetrics' => Inertia::defer(fn () => Metric::calculateQuarterlyRevenue()),
        ]);
    }
}

Frontend: Penataan Boundary <Deferred>

Gunakan helper <Deferred> untuk mendefinisikan fallback yang sinkron antara SSR dan hidrasi klien.

<script setup>
import { Deferred } from '@inertiajs/vue3';
import MetricCard from './MetricCard.vue';
import MetricSkeleton from './MetricSkeleton.vue';

defineProps({
    fastStats: Object,
    revenueMetrics: Object,
});
</script>

<template>
    <div class="dashboard-grid">
        <div class="stat-box">
            <p>Active Users: {{ fastStats.activeUsers }}</p>
        </div>

        <!-- Wrapper menahan dimensi struktural -->
        <div class="deferred-card-wrapper">
            <Deferred data="revenueMetrics">
                <template #fallback>
                    <!-- Skeleton memiliki tag root <div> dan kelas identik dengan target -->
                    <MetricSkeleton />
                </template>
                <MetricCard :data="revenueMetrics" />
            </Deferred>
        </div>
    </div>
</template>

Optimasi Lanjutan dengan WhenVisible

Untuk komponen di bawah lipatan layar (below-the-fold), jangan gunakan deferred prop standar yang langsung dieksekusi setelah halaman termuat. Gunakan komponen WhenVisible milik Inertia v2 untuk menghemat bandwidth dan thread JavaScript.

<script setup>
import { WhenVisible } from '@inertiajs/vue3';
import ComplexChart from './ComplexChart.vue';
import ChartSkeleton from './ChartSkeleton.vue';
</script>

<template>
    <WhenVisible data="chartData" :buffer="200">
        <template #fallback>
            <ChartSkeleton />
        </template>
        <ComplexChart :data="chartData" />
    </WhenVisible>
</template>

Atribut buffer="200" memulai prefetching data 200 piksel sebelum elemen masuk ke viewport, meniadakan skeleton flashing tanpa membebani initial hydration.

Automated Testing: Validasi Hidrasi Tanpa Peringatan Konsol

Mendeteksi hydration mismatch secara manual rentan terlewat. Gunakan Playwright untuk memverifikasi bahwa hidrasi SSR berjalan bersih tanpa memunculkan error atau warning di browser console.

import { test, expect } from '@playwright/test';

test('SSR hydration selesai tanpa warning console', async ({ page }) => {
    const consoleMessages: string[] = [];

    page.on('console', (msg) => {
        if (msg.type() === 'error' || msg.type() === 'warning') {
            consoleMessages.push(msg.text());
        }
    });

    await page.goto('/dashboard');
    
    // Pastikan fallback render terlebih dahulu lalu data aktual masuk
    await page.waitForSelector('.deferred-card-wrapper');
    await page.waitForResponse(resp => resp.url().includes('revenueMetrics'));

    // Evaluasi log console untuk mendeteksi error hidrasi Vue/React
    const hydrationErrors = consoleMessages.filter((msg) =>
        msg.toLowerCase().includes('hydration') ||
        msg.toLowerCase().includes('mismatch')
    );

    expect(hydrationErrors).toEqual([]);
});

Ringkasan Tindakan

  1. Pastikan root elemen komponen fallback dan komponen final menggunakan tag HTML, display CSS, dan padding yang sama.
  2. Kunci ruang tata letak menggunakan CSS min-height atau aspect-ratio untuk meredam CLS akibat penggantian skeleton.
  3. Gunakan WhenVisible dengan parameter buffer untuk prop yang tidak esensial di viewport awal.
  4. Jalankan end-to-end integration test via CI pipeline untuk memantau pesan log hidrasi secara otomatis.