Lazy Widget
Lazy widget mengirim definition-nya bersama response page, sedangkan datanya dikirim melalui request lanjutan. Gunakan mekanisme ini ketika query widget cukup lambat sehingga menahan seluruh dashboard sampai query tersebut selesai bukan pilihan yang tepat — misalnya grouped aggregate atas data satu tahun, count melalui join, atau query lain yang tidak seharusnya berada di critical path first paint.
Mekanismenya menggunakan deferred props milik Inertia. Satu deferred prop membawa payload semua lazy widget pada page, menggunakan widget id sebagai key, dan renderer menampilkan skeleton sampai payload tersebut tersedia.
Contoh minimal yang dapat langsung digunakan
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Widgets;
use PandaPanel\Widgets\ChartWidget;
use PandaPanel\Widgets\Support\ChartSeries;
final class UserGrowth extends ChartWidget
{
protected static bool $lazy = true;
public function labels(): array { /* ... */ }
public function series(): array { /* ... */ }
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Itulah seluruh API yang dibutuhkan. Page dapat dirender segera dengan skeleton pada cell tersebut, lalu chart muncul setelah datanya diterima.
API
| Member | Signature | Default |
|---|---|---|
$lazy | protected static bool | false |
isLazy() | public static function isLazy(): bool | $lazy |
use App\Panels\Admin\Widgets\UserGrowth;
UserGrowth::isLazy(); // true2
3
Lazy loading nonaktif secara default karena membutuhkan request kedua. Widget yang dapat menghasilkan data dalam beberapa milidetik sebaiknya tetap eager dan langsung mengembalikan datanya.
Apa yang terjadi pada request/response
Response pertama
Definition diserialisasi tanpa data widget:
[
'id' => 'user-growth',
'type' => 'chart',
'lazy' => true,
'heading' => 'Sign-ups',
'data' => null, // withheld
// ...
]2
3
4
5
6
7
8
dan deferred prop sama sekali tidak ada di dalam props object. Bukan bernilai null — key tersebut memang absent. Inertia::defer() mengeluarkannya dari initial payload, sehingga setiap Vue component yang membacanya harus mendeklarasikan prop tersebut sebagai optional:
withDefaults(
defineProps<{
widgets: WidgetDefinition[];
widgetData?: WidgetData | null;
}>(),
{ widgetData: null },
);2
3
4
5
6
7
Jika prop dibuat required, setiap first paint dashboard akan menghasilkan warning.
Request lanjutan
Inertia melakukan partial reload yang hanya meminta prop tersebut:
GET /admin
X-Inertia: true
X-Inertia-Version: {asset version}
X-Inertia-Partial-Component: panel/Dashboard
X-Inertia-Partial-Data: widgetData2
3
4
5
dan response membawa payload seluruh lazy widget dengan id widget sebagai key:
{
"props": {
"widgetData": {
"user-growth": { "variant": "area", "labels": ["Apr"], "series": [], "options": {}, "maxHeight": 200 }
}
}
}2
3
4
5
6
7
Renderer membaca widget.data ?? resolved[widget.id] ?? null. Karena itu eager widget menggunakan data inline miliknya, sedangkan lazy widget mengambil data dari deferred payload.
Apa yang dilihat pengguna selama menunggu
WidgetRenderer.vue menggambar skeleton yang disesuaikan dengan type widget yang sedang ditunggu:
| Type | Skeleton |
|---|---|
| stats | tiga card |
| table | lima row |
| chart | empat row |
| custom | tiga row |
Heading, description, dan filter widget langsung dirender karena bagian tersebut berada dalam definition, bukan data. Dengan begitu layout page tidak berubah ketika payload tiba. Component milik custom widget juga belum di-mount sebelum data tersedia, sehingga prop-nya tidak berada dalam kondisi undefined.
Satu prop untuk semua widget
public function deferred(): mixedPandaPanel\Pages\WidgetCollection::deferred() mengembalikan satu closure Inertia::defer() yang mencakup seluruh lazy widget pada page, atau null jika tidak ada widget yang lazy. Dengan demikian page yang tidak memiliki data deferred tidak mengiklankan request kedua yang sebenarnya tidak dibutuhkan.
Resource page menggabungkan collection header dan footer untuk kebutuhan ini:
$header = WidgetCollection::for($this->headerWidgets(), $context);
$footer = WidgetCollection::for($this->footerWidgets(), $context);
return [
'headerWidgets' => $header->definitions(),
'footerWidgets' => $footer->definitions(),
'widgetData' => $header->merge($footer)->deferred(),
];2
3
4
5
6
7
8
Menggunakan satu prop, bukan satu prop per widget, merupakan trade-off yang disengaja: page hanya membutuhkan satu request tambahan daripada lima request terpisah, dengan konsekuensi semua lazy widget muncul bersama-sama alih-alih satu per satu.
Otorisasi tetap dijalankan terlebih dahulu
canView() diperiksa sebelum widget dibuat. Widget yang tidak boleh dilihat pengguna tidak dimasukkan ke definitions maupun deferred payload, dan data() miliknya tidak pernah dijalankan:
WidgetCollection::for([ForbiddenStatsWidget::class])->definitions(); // []
WidgetCollection::for([ForbiddenStatsWidget::class])->deferred(); // null2
Deferred loading tidak mengurangi jaminan tersebut. Lihat Otorisasi.
Testing lazy widget
Lakukan assertion bahwa data ditahan pada response pertama dan tersedia pada request lanjutan. Package mengujinya dengan pola berikut:
it('withholds a lazy widget payload from the first response', function (): void {
$this->get('/admin')->assertInertia(function (AssertableInertia $page): void {
$widget = collect($page->toArray()['props']['widgets'])
->firstWhere('id', UserGrowth::id());
expect($widget['lazy'])->toBeTrue()
->and($widget['data'])->toBeNull();
});
});
it('omits the deferred prop entirely from the first response', function (): void {
$this->get('/admin')->assertInertia(function (AssertableInertia $page): void {
expect($page->toArray()['props'])->not->toHaveKey('widgetData');
});
});
it('resolves the lazy payload on the follow-up request', function (): void {
// A partial reload must send the asset version, otherwise Inertia answers 409
// and asks the browser to do a full visit instead.
$version = $this->get('/admin')->viewData('page')['version'];
$this->get('/admin', [
'X-Inertia' => 'true',
'X-Inertia-Version' => $version,
'X-Inertia-Partial-Component' => 'panel/Dashboard',
'X-Inertia-Partial-Data' => 'widgetData',
])
->assertOk()
->assertJsonPath('props.widgetData.'.UserGrowth::id().'.variant', 'area');
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Anda juga dapat memanggil widget secara langsung. Cara ini melewati mekanisme lazy sepenuhnya karena data() tidak mengetahui dan tidak perlu mengetahui bagaimana hasilnya akan dikirim:
expect((new UserGrowth)->data()['maxHeight'])->toBe(200);Hal yang perlu diperhatikan
- Deferred prop digunakan bersama, sehingga lazy widget paling lambat pada sebuah page menentukan kapan semua lazy widget tampil. Jika ada dua widget, satu cepat dan satu lambat, biasanya lebih baik membuat satu eager dan satu lazy.
X-Inertia-Versionharus dikirim pada follow-up request. Jika tidak, Inertia mengembalikan409dan meminta browser melakukan full visit. Browser menangani ini otomatis, tetapi test yang dibuat manual harus mengirim header tersebut.- Sebuah poll melakukan reload
['widgets', 'widgetData'], sehingga lazy payload akan di-resolve ulang pada setiap polling. Widget yang sekaligus lazy dan polling akan tetap menjalankan slow query pada setiap interval. $lazybersifat static. Widget tidak dapat memutuskan menjadi lazy hanya untuk pengguna tertentu.- Lazy loading hanya mengubah kapan query dijalankan, bukan apakah query dijalankan. Ini bukan cache dan bukan query budget — query lambat tetap lambat, hanya dipindahkan keluar dari critical path initial render.
- Lazy data tidak pernah disimpan oleh
panel:cache. Manifest hanya meng-cache nama class. Lihat Caching.