Fitur deferred props pada Inertia.js v2 memungkinkan backend menunda resolusi data berat di luar siklus initial request. Ketika dipadukan dengan Server-Side Rendering (SSR) berbasis Node.js, mekanisme ini rentan menimbulkan hydration mismatch dan skeleton drift (pergeseran layout/Cumulative Layout Shift) jika siklus sinkronisasi state antara SSR engine dan client hydration tidak dikelola secara presisi.

Root Cause: Perbedaan VDOM SSR vs Client Hydration

Hydration mismatch terjadi ketika representasi Virtual DOM (VDOM) yang dihasilkan oleh render awal di client tidak identik dengan struktur HTML yang di-generate oleh proses Node.js SSR. Alur eksekusi deferred props melibatkan tahapan berikut:

  1. SSR Render Pass: Engine SSR Node.js mengeksekusi controller Laravel. Properti yang dibungkus Inertia::defer() tidak dievaluasi ke dalam data payload awal, melainkan dikirim sebagai instruksi defer. Node.js me-render fallback skeleton ke dalam static HTML.
  2. Client Initial Hydration: Browser menerima HTML statis beserta page payload Inertia. Pada tahap ini, deferred prop masih bernilai undefined. Client runtime (Vue 3 atau React) merekonstruksi VDOM berdasarkan payload tersebut dan mengaitkannya ke DOM fisik hasil SSR.
  3. Deferred Fetch: Segera setelah hydration selesai, Inertia client runtime menembak request parsial untuk mengambil deferred props, lalu me-re-render komponen dengan data sebenarnya.

Kegagalan hydration terjadi jika komponen membaca state lingkungan yang tidak deterministik pada langkah ke-2 (misalnya manipulasi state lokal prematur, pemanggilan variabel window di dalam conditional rendering, atau perbedaan penanganan tag pembungkus fragmen antara client dan server).

Implementasi: Laravel Controller & Inertia::defer()

Contoh implementasi endpoint analitik pada Laravel controller:

namespace App\Http\Controllers;

use App\Services\AnalyticsService;
use Inertia\Inertia;
use Inertia\Response;

class AnalyticsController extends Controller
{
    public function index(AnalyticsService $service): Response
    {
        return Inertia::render('Analytics/Dashboard', [
            'summary' => [
                'active_projects' => 12,
                'team_members' => 4,
            ],
            // Data berat dieksekusi secara asinkron pasca-render awal
            'revenueMetrics' => Inertia::defer(fn () => $service->calculateHeavyRevenue()),
        ]);
    }
}

Isolasi Boundary Komponen pada Vue 3

Inertia v2 menyediakan komponen wrapper bawaan <Deferred>. Untuk mencegah mismatch, fallback skeleton harus terisolasi di dalam slot fallback tanpa bergantung pada lifecycle hook yang memanipulasi DOM sebelum hydration tuntas.

<script setup lang="ts">
import { Deferred } from '@inertiajs/vue3';

interface RevenueData {
    total_revenue: number;
    arr: number;
}

defineProps<{
    summary: {
        active_projects: number;
        team_members: number;
    };
    revenueMetrics?: RevenueData;
}>();
</script>

<template>
    <div class="dashboard-grid">
        <div class="card">
            <h3>Summary</h3>
            <p>Active: {{ summary.active_projects }}</p>
        </div>

        <!-- Isolasi boundary menggunakan Deferred component -->
        <Deferred data="revenueMetrics">
            <template #fallback>
                <div class="card card-skeleton" aria-hidden="true">
                    <div class="skeleton-line skeleton-title"></div>
                    <div class="skeleton-line skeleton-metric"></div>
                </div>
            </template>

            <div class="card card-metric" v-if="revenueMetrics">
                <h3>Total Revenue</h3>
                <p class="value">${{ revenueMetrics.total_revenue.toLocaleString() }}</p>
            </div>
        </Deferred>
    </div>
</template>
Catatan: Hindari penggunaan conditional non-deterministik seperti v-if="typeof window !== 'undefined' && revenueMetrics". Ini secara langsung membedakan hasil kompilasi SSR dan client, yang memicu peringatan Hydration node mismatch.

Mengatasi Skeleton Drift & CLS dengan CSS Modern

Skeleton drift terjadi saat ukuran fisik placeholder tidak sesuai dengan ukuran elemen akhir setelah deferred props tiba, menyebabkan Cumulative Layout Shift (CLS). Atasi dengan mengunci dimensi menggunakan CSS aspect-ratio atau CSS Container Queries.

/* Mencegah layout shift dengan mengunci baseline dimension */
.card {
    container-type: inline-size;
    min-height: 140px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    box-sizing: border-box;
}

.card-skeleton {
    pointer-events: none;
}

.skeleton-line {
    background: #e2e8f0;
    border-radius: 4px;
    animation: pulse 1.5s infinite;
}

.skeleton-title {
    height: 1.25rem;
    width: 40%;
    margin-bottom: 0.5rem;
}

.skeleton-metric {
    height: 2.25rem;
    width: 70%;
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}

Dengan mendefinisikan min-height dan rasio tetap yang identik antara .card-skeleton dan .card-metric, browser mengalokasikan ruang rendering yang sama di VDOM SSR sebelum dan sesudah data deferred diterima.

Validasi Hydration Pass via Console Logging

Di lingkungan production, hydration mismatch sering kali terdegradasi secara diam-diam (bailout) ke full client render, yang merusak kinerja first-load. Tangkap error tersebut di file bootstrap client (app.ts):

import { createSSRApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';

createInertiaApp({
    resolve: (name) => require(`./Pages/${name}.vue`),
    setup({ el, App, props, plugin }) {
        const app = createSSRApp({ render: () => h(App, props) });

        // Intersept peringatan hydration mismatch pada dev & staging
        app.config.warnHandler = (msg, instance, trace) => {
            if (msg.includes('hydration')) {
                console.error(`[Hydration Failure]: ${msg}`, trace);
            }
        };

        app.use(plugin).mount(el);
    },
});

Pada implementasi React (createRoot / hydrateRoot), gunakan callback onRecoverableError pada hydrateRoot untuk mengirim log mismatch ke pipeline APM Anda.