Pada aplikasi Laravel yang menggunakan Inertia.js, masalah performa sering tidak muncul karena proses rendering frontend, melainkan karena payload props yang terlalu besar, query database yang tidak perlu, dan kebiasaan me-render ulang seluruh halaman ketika hanya sebagian data yang berubah. Inertia memang membuat pengalaman membangun aplikasi modern menjadi lebih sederhana, tetapi jika semua data dashboard, tabel, statistik, dan widget selalu dikirim ulang pada setiap navigasi, maka waktu respons akan membengkak dan pengalaman pengguna terasa berat.

Artikel ini membahas teknik optimasi performa Inertia.js yang paling praktis di Laravel: partial reload, lazy evaluation props, deferred props, serta penggunaan preserveState dan preserveScroll. Kita juga akan melihat studi kasus dashboard dengan beberapa widget berat agar dampak pengurangan payload lebih mudah dipahami.

Mengapa performa Inertia.js bisa menurun?

Secara konsep, Inertia.js mengirim data halaman dalam bentuk props dari server ke client. Setiap kali pengguna berpindah halaman atau melakukan kunjungan ulang ke halaman yang sama dengan filter berbeda, server dapat mengembalikan komponen yang sama beserta props terbaru. Pola ini nyaman, tetapi ada konsekuensi: jika props yang dikirim terlalu banyak, maka biaya berikut ikut naik:

  • Waktu query backend, karena controller menghitung data yang mungkin tidak sedang dibutuhkan.
  • Ukuran respons HTTP, karena semua props diserialisasi menjadi JSON.
  • Waktu parsing di browser, terutama jika data besar seperti daftar aktivitas, grafik, agregasi statistik, dan tabel detail dikirim bersamaan.
  • Re-render komponen, karena banyak state UI ikut diperbarui meskipun pengguna hanya mengubah sebagian kecil konteks halaman.

Masalah ini biasanya muncul pada halaman dashboard, index dengan filter kompleks, atau halaman laporan yang memiliki banyak panel. Dalam situasi seperti itu, optimasi tidak cukup hanya di level query database. Anda juga perlu mengontrol kapan props dihitung dan prop mana yang benar-benar dikirim.

Studi kasus: dashboard dengan widget berat

Bayangkan sebuah dashboard admin memiliki beberapa widget berikut:

  • Ringkasan metrik utama: total user, order hari ini, revenue bulanan.
  • Grafik revenue 12 bulan.
  • Daftar 20 transaksi terbaru.
  • Daftar notifikasi sistem.
  • Top products berdasarkan penjualan.
  • Aktivitas audit log terbaru.

Secara naif, controller bisa saja menghitung semuanya pada setiap request:

use Inertia\Inertia;
use App\Models\Order;
use App\Models\User;
use App\Models\Product;
use App\Models\AuditLog;
use App\Models\Notification;

public function index()
{
    return Inertia::render('Dashboard/Index', [
        'summary' => [
            'users' => User::count(),
            'ordersToday' => Order::whereDate('created_at', today())->count(),
            'monthlyRevenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
        ],
        'revenueChart' => $this->revenueChart(),
        'recentOrders' => Order::latest()->with('customer')->take(20)->get(),
        'notifications' => Notification::latest()->take(10)->get(),
        'topProducts' => Product::query()
            ->withSum('orderItems', 'quantity')
            ->orderByDesc('order_items_sum_quantity')
            ->take(10)
            ->get(),
        'auditLogs' => AuditLog::latest()->take(30)->get(),
    ]);
}

Pendekatan ini sederhana, tetapi mahal. Misalnya pengguna hanya mengganti filter tanggal pada grafik revenue. Apakah daftar audit log, top products, dan notifikasi harus ikut dihitung dan dikirim lagi? Dalam banyak kasus, jawabannya tidak.

Partial reload: kirim ulang hanya props yang dibutuhkan

Partial reload memungkinkan client meminta hanya sebagian props ketika melakukan kunjungan ke halaman yang sama. Ini sangat berguna untuk interaksi seperti filter, sorting, tab, atau refresh satu widget tertentu tanpa memuat ulang seluruh payload halaman.

Di sisi client, Anda dapat menggunakan router.reload() atau router.visit() dengan opsi only:

import { router } from '@inertiajs/vue3'

function reloadRevenueRange(range) {
  router.reload({
    only: ['revenueChart', 'summary'],
    data: { range },
    preserveState: true,
    preserveScroll: true,
  })
}

Dengan cara ini, browser tidak meminta semua props lagi. Server juga dapat menghindari evaluasi props yang tidak diminta, terutama jika digabung dengan lazy props. Hasilnya adalah respons lebih kecil dan lebih cepat.

Kapan dipakai?

  • Saat pengguna mengubah filter pada satu bagian halaman.
  • Saat ada tab atau panel yang isinya independen dari widget lain.
  • Saat ingin me-refresh statistik tertentu tanpa mengganggu state form atau scroll.

Kesalahan umum adalah mengira partial reload otomatis mengurangi beban backend. Kenyataannya, jika semua props tetap dihitung langsung di controller, maka server masih mengerjakan semua query. Partial reload akan maksimal jika digabung dengan evaluasi lazy.

Lazy evaluation props: hitung hanya jika memang diminta

Di Inertia, props dapat diberikan sebagai closure. Closure ini baru dievaluasi ketika props tersebut benar-benar diperlukan untuk respons. Inilah kunci penting agar partial reload tidak hanya mengecilkan payload, tetapi juga mengurangi kerja backend.

public function index()
{
    return Inertia::render('Dashboard/Index', [
        'summary' => fn () => [
            'users' => User::count(),
            'ordersToday' => Order::whereDate('created_at', today())->count(),
            'monthlyRevenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
        ],
        'revenueChart' => fn () => $this->revenueChart(request('range', '30d')),
        'recentOrders' => fn () => Order::latest()->with('customer:id,name')->take(20)->get(),
        'notifications' => fn () => Notification::latest()->take(10)->get(['id', 'title', 'created_at']),
        'topProducts' => fn () => Product::query()
            ->select('id', 'name')
            ->withSum('orderItems', 'quantity')
            ->orderByDesc('order_items_sum_quantity')
            ->take(10)
            ->get(),
        'auditLogs' => fn () => AuditLog::latest()->take(30)->get(['id', 'event', 'created_at']),
    ]);
}

Mengapa ini efektif? Karena ketika client melakukan partial reload dengan only: ['revenueChart', 'summary'], maka closure untuk recentOrders, topProducts, dan auditLogs tidak perlu dieksekusi. Jadi penghematan terjadi di dua sisi: query database lebih sedikit dan respons lebih kecil.

Kapan dipakai?

  • Hampir selalu untuk props yang mahal dihitung.
  • Pada halaman yang punya banyak panel independen.
  • Ketika props berisi hasil agregasi, relasi, atau dataset menengah hingga besar.

Trade-off: jika semua props dibungkus closure tanpa disiplin struktur, controller bisa menjadi sulit dibaca. Solusinya, pindahkan logika berat ke service class, query object, atau action class agar controller tetap ringkas.

Deferred props: tunda data non-kritis agar halaman tampil lebih cepat

Deferred props digunakan ketika ada data yang tidak harus tersedia pada render awal. Tujuannya bukan sekadar mengecilkan payload, tetapi mempercepat first meaningful render halaman. Data non-kritis bisa diminta setelah halaman dasar sudah tampil.

Ini cocok untuk widget yang penting tetapi tidak memblokir interaksi awal, seperti audit log, notifikasi tambahan, atau panel analitik sekunder.

Contoh pada controller:

use Inertia\Inertia;

public function index()
{
    return Inertia::render('Dashboard/Index', [
        'summary' => fn () => $this->summary(),
        'revenueChart' => fn () => $this->revenueChart(request('range', '30d')),
        'recentOrders' => fn () => $this->recentOrders(),
        'notifications' => Inertia::defer(fn () => $this->notifications()),
        'topProducts' => Inertia::defer(fn () => $this->topProducts()),
        'auditLogs' => Inertia::defer(fn () => $this->auditLogs()),
    ]);
}

Di sisi komponen, tampilkan skeleton atau placeholder selama data deferred belum tersedia:

<script setup>
import { computed } from 'vue'
import { usePage } from '@inertiajs/vue3'

const page = usePage()
const notifications = computed(() => page.props.notifications)
const topProducts = computed(() => page.props.topProducts)
</script>

<template>
  <section>
    <DashboardSummary :data="page.props.summary" />
    <RevenueChart :data="page.props.revenueChart" />

    <TopProducts v-if="topProducts" :items="topProducts" />
    <WidgetSkeleton v-else />

    <NotificationList v-if="notifications" :items="notifications" />
    <WidgetSkeleton v-else />
  </section>
</template>

Kapan dipakai?

  • Untuk widget sekunder yang tidak diperlukan agar halaman dapat langsung dipakai.
  • Pada dashboard besar yang ingin segera menampilkan metrik inti lebih dulu.
  • Untuk data yang relatif berat tetapi tetap dibutuhkan setelah halaman muncul.

Jangan menunda semua props. Jika data utama yang dilihat pengguna justru di-defer, halaman akan terasa kosong dan pengalaman pengguna menurun.

PreserveState dan preserveScroll: optimasi UX saat reload parsial

Optimasi performa tidak hanya soal milidetik. Pengalaman pengguna juga penting. Dua opsi yang sering dipakai bersama partial reload adalah preserveState dan preserveScroll.

PreserveState

preserveState: true mempertahankan state lokal komponen ketika request Inertia selesai. Ini berguna untuk form filter, tab aktif, panel yang sedang terbuka, atau komponen pencarian yang tidak ingin kembali ke state awal.

router.reload({
  only: ['recentOrders'],
  data: { status: selectedStatus.value },
  preserveState: true,
})

Kapan dipakai?

  • Pada filter tabel atau dashboard control panel.
  • Saat komponen memiliki state lokal yang seharusnya tidak di-reset.
  • Pada interaksi yang terasa seperti pembaruan data, bukan navigasi penuh.

Hati-hati: preserve state dapat menyimpan state UI yang seharusnya ikut berubah jika struktur halaman benar-benar berbeda. Gunakan terutama pada halaman yang sama dengan perubahan data, bukan perpindahan konteks besar.

PreserveScroll

preserveScroll: true mencegah halaman kembali ke posisi atas setelah request selesai. Ini sangat membantu jika pengguna sedang berada di bagian bawah dashboard atau di tabel panjang.

router.reload({
  only: ['auditLogs'],
  preserveState: true,
  preserveScroll: true,
})

Kapan dipakai?

  • Pada filter, refresh widget, pagination lokal, atau muat ulang sebagian daftar.
  • Saat halaman panjang dan pengguna tidak boleh kehilangan konteks visualnya.

Jangan pakai secara buta pada navigasi antarhalaman utama, karena kadang perilaku scroll reset justru lebih natural.

Pola implementasi yang disarankan untuk dashboard berat

Untuk dashboard dengan banyak widget, pola berikut umumnya efektif:

  1. Render awal: kirim hanya metrik inti dan widget yang wajib terlihat segera.
  2. Lazy props: bungkus hampir semua query berat dalam closure.
  3. Deferred props: tunda widget sekunder seperti audit log atau top products.
  4. Partial reload: saat pengguna mengubah filter grafik, request hanya grafik dan ringkasan terkait.
  5. Preserve state/scroll: aktifkan pada interaksi in-page agar UI tidak terasa melompat.

Contoh di client untuk filter rentang tanggal grafik:

import { ref } from 'vue'
import { router } from '@inertiajs/vue3'

const range = ref('30d')

function updateRange(value) {
  range.value = value

  router.reload({
    only: ['summary', 'revenueChart'],
    data: { range: value },
    preserveState: true,
    preserveScroll: true,
  })
}

Pola ini membuat perubahan pada grafik tidak ikut memicu pengiriman ulang audit log, top products, dan notifikasi.

Tips profiling request, response, dan query

Optimasi sebaiknya berbasis observasi, bukan asumsi. Berikut beberapa tips praktis:

1. Cek ukuran respons di browser DevTools

Buka tab Network, pilih request Inertia, lalu periksa:

  • Payload size atau transfer size.
  • Durasi request.
  • Apakah props yang tidak relevan masih ikut terkirim.

Jika setiap perubahan filter menghasilkan respons besar, itu tanda partial reload belum dimanfaatkan dengan benar.

2. Profiling query di Laravel

Gunakan Laravel Debugbar, Telescope, atau logging query untuk melihat query mana yang dieksekusi pada setiap reload. Tujuannya memastikan lazy props memang mencegah query yang tidak diperlukan.

DB::listen(function ($query) {
    logger()->info('sql', [
        'sql' => $query->sql,
        'bindings' => $query->bindings,
        'time' => $query->time,
    ]);
});

Jika partial reload hanya meminta revenueChart tetapi query audit_logs masih jalan, kemungkinan Anda masih menghitung data tersebut secara eager.

3. Kurangi data yang diserialisasi

Selain strategi Inertia, pastikan Anda juga hanya memilih kolom yang diperlukan. Hindari mengirim seluruh model beserta relasi jika yang dibutuhkan hanya beberapa field.

  • Gunakan select() atau kolom eksplisit pada get().
  • Batasi relasi dengan kolom tertentu.
  • Gunakan API resource jika struktur data perlu dikendalikan dengan ketat.

4. Perhatikan N+1 query

Lazy props tidak akan membantu jika query di dalamnya sendiri tidak efisien. Selalu cek relasi yang perlu di-with() dan agregasi yang bisa dihitung lebih efisien di database.

Kapan memakai fitur yang mana?

  • Partial reload: ketika hanya sebagian props perlu diperbarui akibat filter, sorting, tab, atau refresh widget.
  • Lazy props: ketika props mahal dihitung dan Anda ingin eksekusi query hanya terjadi jika props diminta.
  • Deferred props: ketika data tidak kritis untuk render awal dan bisa dimuat belakangan.
  • PreserveState: ketika state lokal komponen harus dipertahankan selama reload parsial.
  • PreserveScroll: ketika posisi scroll pengguna tidak boleh berubah setelah pembaruan data.

Prinsip sederhananya: partial reload mengontrol apa yang diminta client, lazy props mengontrol apa yang dihitung server, dan deferred props mengontrol apa yang ditunda dari render awal.

Penutup

Optimasi performa Inertia.js di Laravel bukan soal trik tunggal, melainkan kombinasi beberapa fitur yang saling melengkapi. Jika Anda hanya melakukan partial reload tanpa lazy props, server masih bisa tetap berat. Jika Anda menunda semua widget dengan deferred props, halaman bisa terasa kosong. Jika Anda mengabaikan preserveState dan preserveScroll, optimasi teknis justru bisa menurunkan kenyamanan penggunaan.

Untuk halaman dashboard dengan widget berat, pendekatan yang paling aman adalah: tampilkan data inti lebih dulu, tunda data sekunder, hitung props secara lazy, dan reload hanya bagian yang memang berubah. Setelah itu, validasi hasilnya lewat profiling ukuran respons, waktu request, dan query database. Dengan pola ini, Anda tidak hanya mengurangi payload, tetapi juga membuat aplikasi Inertia terasa lebih responsif dan efisien dalam skala nyata.