Keep your Filament admin dashboard fast by lazy loading widgets

November 7th, 2022

Filament is a great admin panel for Laravel built with Livewire, one of its features is creating dashboard widgets to get quick stats of your application.

One issue we were having is that some of the widgets could take up to a second to load all the data needed. If you're just visiting the dashboard, you don't want to wait on all the stats just because one of them is loading slowly.

Lazy loading Filament widgets

With Livewire, you can easily defer loading of something until the page was visited. We'll be applying this concept to the "Stats overview widget" of Filament. You can apply this to any other widget as well.

Creating the widget

First off, we'll create our widget, which we'll call "SlowWidget". When Filament asks for a resource, just press <Enter>

php artisan make:filament-widget SlowWidget --stats-overview

This gives us the following PHP class:

<?php

namespace App\Filament\Widgets;

use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Card;

class SlowWidget extends BaseWidget
{
    protected function getCards(): array
    {
        return [
            //
        ];
    }
}

Adding our "slow" query

Now we'll simulate slow retrieval of data by adding a small sleep() call inside the getCards() method.

<?php

namespace App\Filament\Widgets;

use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Card;

class SlowWidget extends BaseWidget
{
    protected function getCards(): array
    {
        sleep(5);

        return [
            Card::make('Total', 10),
        ];
    }
}

You'll notice that loading the dashboard now takes 5 seconds before you get to see anything.

Implementing Livewire's defer loading

Adding Livewire's defer loading to our widget is just a few lines of extra code. First we'll define the loadData() method and $readyToLoad property on our widget.

<?php

namespace App\Filament\Widgets;

use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Card;

class SlowWidget extends BaseWidget
{
    public bool $readyToLoad = false;

    public function loadData()
    {
        $this->readyToLoad = true;
    }
    
    protected function getCards(): array
    {
        sleep(5);

        return [
            Card::make('Total', 10),
        ];
    }
}

We'll need to change our getCards() method to take into account that we're ready to load the data or not:

<?php

namespace App\Filament\Widgets;

use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Card;

class SlowWidget extends BaseWidget
{
    public bool $readyToLoad = false;

    public function loadData()
    {
        $this->readyToLoad = true;
    }

    protected function getCards(): array
    {
        if (! $this->readyToLoad) {
            return [
                Card::make('Total', 'loading...'),
            ];
        }

        sleep (5);

        return [
            Card::make('Total', 10),
        ];
    }
}

The dashboard should now be fast again, but you'll notice that the widget now stays stuck on "loading...", even if you wait for 5 seconds or longer for it to load.

The last step we need is to override the stats-overview-widget.blade.php of Filament to have it call our loadData() method, you can publish the Filament views, or just create the file at resources/vendor/filament/widgets/stats-overview-widget.blade.php

By default it looks like this:

<x-filament::widget class="filament-stats-overview-widget">
    <div {!! ($pollingInterval = $this->getPollingInterval()) ? "wire:poll.{$pollingInterval}" : '' !!}>
        <x-filament::stats :columns="$this->getColumns()">
            @foreach ($this->getCachedCards() as $card)
                {{ $card }}
            @endforeach
        </x-filament::stats>
    </div>
</x-filament::widget>

We'll make one small addition, note the wire:init="loadData" add the end of the x-filament:widget tag:

 <x-filament::widget class="filament-stats-overview-widget" wire:init="loadData">
    <div {!! ($pollingInterval = $this->getPollingInterval()) ? "wire:poll.{$pollingInterval}" : '' !!}>
        <x-filament::stats :columns="$this->getColumns()">
            @foreach ($this->getCachedCards() as $card)
                {{ $card }}
            @endforeach
        </x-filament::stats>
    </div>
</x-filament::widget>

And that's it, the widget now displays 10 as the value after 5 seconds, but your dashboard stays snappy!

You can like or retweet this Tweet
MENU