Filament traag? Performance bij honderdduizenden records
Terug naar blog

Filament traag? Performance bij honderdduizenden records

10 min leestijd

Een traag Filament-panel ligt zelden aan Filament zelf. In de praktijk zijn het drie oorzaken: N+1-queries, ontbrekende database-indexes en een COUNT() over honderdduizenden rijen bij elke paginaweergave. Los je die op, gecombineerd met deferLoading() en simple of cursor pagination, dan voelt...

Een traag Filament-panel ligt zelden aan Filament zelf. In de praktijk zijn het drie oorzaken: N+1-queries, ontbrekende database-indexes en een COUNT() over honderdduizenden rijen bij elke paginaweergave. Los je die op, gecombineerd met deferLoading() en simple of cursor pagination, dan voelt een tabel met 500.000 records vrijwel net zo snel als een tabel met 500.

Onze eigen backoffice draait op Filament. Daarin staat onder meer een database met Magento-shops die flink groeit. Dit is de checklist die wij aflopen als een tabel traag aanvoelt, met API-namen uit de Filament v4/v5-documentatie.

Waarom is mijn Filament-tabel traag?

Meestal komt het door de database, niet door de PHP-code of de Blade-rendering. Meet eerst. Pas daarna ga je optimaliseren.

Een Filament-tabel doet bij elke request grofweg vier dingen:

  1. Een COUNT()-query voor de paginering ("toont 1 tot 25 van 482.113").
  2. Een SELECT met LIMIT/OFFSET voor de huidige pagina.
  3. Extra queries voor relaties, aggregaten en state van acties.
  4. Het renderen van de HTML via Livewire.

Bij honderdduizenden records worden stap 1 en 3 de bottleneck. Een LIKE '%zoekterm%' op een tekstkolom zonder bruikbare index doet de rest.

SymptoomWaarschijnlijke oorzaakEerste ingreep
Elke pagina traag, ook pagina 1COUNT() over hele tabelPaginationMode::Simple
Pagina 1 snel, pagina 2.000 traagGrote OFFSETPaginationMode::Cursor
Honderden queries per requestN+1 op relatiesDot notation, counts(), eager loading
Sorteren of filteren traagOntbrekende indexSamengestelde index op filter + sort
Zoeken traagLIKE '%...%' op grote tabelLaravel Scout via searchUsing()
Dashboard voelt zwaarWidgets pollen elke 5 seconden$pollingInterval aanpassen, caching

Hoe tel je queries in een Filament-panel?

Zet in development Model::preventLazyLoading() aan en kijk per request hoeveel queries er draaien. Zonder meting optimaliseer je op gevoel.

Concreet werkt dit voor ons:

  • Laravel Debugbar of Telescope in development. Je ziet per Livewire-request het aantal queries en de traagste query.
  • Model::preventLazyLoading(! app()->isProduction()) in je AppServiceProvider. Elke vergeten relatie gooit dan een exception in plaats van stil 25 extra queries te draaien.
  • DB::listen() in een test of tijdelijke debug-route als je snel wilt tellen.

Een vuistregel: een tabelpagina van 25 rijen hoort met een handvol queries toe te kunnen. Zie je er 60 of meer, dan heb je een N+1-probleem.

Hoe voorkom je N+1-queries in Filament-tabellen?

Gebruik dot notation voor relatiekolommen en counts() voor tellingen. Filament eager-loadt die relaties dan zelf.

Volgens de Filament-documentatie gebruikt Filament de dot notation (author.name) om de relatie automatisch te eager-loaden. Het probleem ontstaat zodra je relatiedata ophaalt via een closure, bijvoorbeeld in formatStateUsing(), description() of een custom view. Daar weet Filament niet welke relatie je nodig hebt.

php
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

public static function table(Table $table): Table
{
    return $table
        ->modifyQueryUsing(fn (Builder $query) => $query->with(['customer', 'store']))
        ->columns([
            TextColumn::make('customer.name'),
            TextColumn::make('items_count')->counts('items'),
            TextColumn::make('store.name')
                ->description(fn ($record) => $record->store->country),
        ]);
}

Drie dingen om te onthouden:

  • counts('items') maakt er één withCount-subquery van. Doe nooit $record->items->count() in een closure: dat laadt alle items per rij.
  • avg(), sum(), min(), max() en exists() werken op dezelfde manier voor aggregaten op relaties.
  • modifyQueryUsing() op de table geldt alleen voor die tabel. Wil je het voor de hele resource, gebruik dan getEloquentQuery() op de Resource.

Global search heeft een eigen query. Toon je relatiedata in de zoekresultaten, override dan getGlobalSearchEloquentQuery() en eager-load daar de relaties. De documentatie noemt lazy loading daar expliciet als oorzaak van slechte performance.

Welke database-indexes heeft een Filament-tabel nodig?

Elke kolom waarop je standaard sorteert, filtert of een tab baseert, hoort in een index. Bij combinaties werkt een samengestelde index het best.

Filament genereert gewone Eloquent-queries. Dus geldt gewone database-logica:

  • Standaard sortering. Sorteer je op created_at desc, dan wil je een index op created_at.
  • Filter + sortering. Filter je op status en sorteer je op created_at, dan is een index op (status, created_at) veel effectiever dan twee losse indexes.
  • Tabs. Tabs met modifyQueryUsing() zijn feitelijk vaste filters. Elke tab-query verdient dezelfde aandacht.
  • Foreign keys. Kolommen als customer_id en store_id hebben een index nodig voor de eager-load-queries.

Draai bij twijfel EXPLAIN op de query uit Debugbar. Staat er type: ALL en een hoog aantal rows, dan scant MySQL de hele tabel.

Wat niemand je vertelt: een index helpt niet bij LIKE '%term%'. Een leading wildcard maakt een gewone B-tree-index onbruikbaar. Daarom is zoeken vaak het laatste trage onderdeel, ook na alle andere ingrepen.

Simple, cursor of gewone paginering: wat kies je?

Kies simple pagination als het totaal aantal records niet belangrijk is. Kies cursor pagination als gebruikers diep door de tabel bladeren. Gewone paginering is prima tot enkele tienduizenden rijen.

Filament ondersteunt drie paginatie-modi via paginationMode():

php
use Filament\Tables\Enums\PaginationMode;
use Filament\Tables\Table;

public static function table(Table $table): Table
{
    return $table
        ->paginationMode(PaginationMode::Simple)
        ->paginated([25, 50, 100])
        ->defaultPaginationPageOption(25);
}
ModusTelt totaal?Springen naar pagina XSnelheid op diepe pagina'sGeschikt voor
Standaard (length-aware)Ja, COUNT()JaDaalt bij grote OFFSETTot tienduizenden records
PaginationMode::SimpleNeeNee, alleen vorige/volgendeDaalt bij grote OFFSETGrote tabellen, gebruikers filteren eerst
PaginationMode::CursorNeeNeeConstantLogs, orders, events, honderdduizenden+

Cursor pagination gebruikt een WHERE id > x in plaats van OFFSET. Daardoor blijft pagina 5.000 net zo snel als pagina 1. Het werkt het best met een vaste sortering op een geïndexeerde, unieke kolom.

Haal daarnaast 'all' uit je paginated()-opties. De Filament-documentatie waarschuwt zelf dat hoge aantallen en all performanceproblemen veroorzaken. Eén gebruiker die op "all" klikt bij 300.000 orders legt je panel plat.

Wat doet deferLoading() en wanneer gebruik je het?

deferLoading() laadt de tabeldata asynchroon na de eerste paginaweergave. De pagina staat er direct; de rijen volgen.
php
return $table->deferLoading();

Dit maakt de query niet sneller. Het verbetert wel de waargenomen snelheid: de gebruiker ziet meteen layout, filters en navigatie. Gebruik het bij tabellen waarvan de query structureel langer dan een paar honderd milliseconden duurt.

Gerelateerd: filters zijn in v4 standaard deferred. Wijzigingen worden pas toegepast na een klik op "Apply". Zet je dit uit met deferFilters(false), dan draait elke wijziging in een filterveld direct een nieuwe query. Bij grote tabellen laat je de standaard dus staan.

Hoe maak je zoeken snel bij honderdduizenden records?

Gebruik Laravel Scout met een zoekengine zoals Meilisearch of Typesense, en koppel die via searchUsing(). Zo verdwijnt de LIKE '%...%'-query uit je database.

Filament heeft geen directe Scout-integratie, maar de documentatie geeft dit patroon:

php
use App\Models\Order;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

public static function table(Table $table): Table
{
    return $table
        ->searchable()
        ->searchUsing(fn (Builder $query, string $search) => $query->whereKey(Order::search($search)->keys()));
}

Scout levert de matchende ID's, Filament filtert de tabel daarop met whereKey(). Volgens de documentatie heeft dat geen performance-nadeel, omdat Scout intern zelf ook whereIn() gebruikt.

Wil je geen extra zoekengine draaien? Dan zijn er tussenoplossingen:

  • Minder searchable kolommen. Elke searchable()-kolom voegt een OR LIKE toe. Zoek op ordernummer en e-mail, niet op tien kolommen.
  • Individuele zoekvelden met searchable(isIndividual: true, isGlobal: false). Dan zoekt een gebruiker gericht in één kolom.
  • Global search beperken. Zet protected static ?bool $shouldSplitGlobalSearchTerms = false; op de Resource om zoektermen niet per woord te splitsen. Verlaag $globalSearchResultsLimit (standaard 50). Of schakel global search uit met ->globalSearch(false) op het panel.

Hoeveel sneller is Filament v4 en v5?

Volgens Filament renderen grote tabellen in v4 typisch 2 tot 3 keer sneller dan in v3. In augustus 2026 kwamen er met v4.12 en v5.7 nog flinke rendering-winsten bij.

De v4-winst komt uit minder Blade-views per tabel en partial rendering. Een actiemodal triggert bijvoorbeeld niet langer een volledige re-render van de tabel. In een voorbeeld op filamentphp.com met 10.000 orders en 100 zichtbare rijen ging de laadtijd van 1,44 seconde naar 603 milliseconden. Het aantal gerenderde Blade-views daalde daarbij van 108 naar 4.

Met v4.12 en v5.7 (6 augustus 2026) meldt Filament voor tabellen van 50 rijen met 5 kolommen en 3 row actions een daling van 27,53 ms naar 13,11 ms. Dat is ruim 50% sneller. Filament zegt er zelf bij dat dit interne benchmarks zijn.

De eerlijke versie: upgraden helpt bij rendering, niet bij een trage query. Een COUNT() van 2 seconden blijft 2 seconden. Draai je nog op v3, dan lees je in Filament 3 upgraden naar 5 hoe je dat aanpakt. Wat er verder veranderde staat in Filament 4 en 5: wat is nieuw.

Hoe houd je de Livewire-payload klein?

Bewaar geen grote collecties of modellen in public properties van je custom Livewire-pagina's. Alles in public state gaat bij elke request heen en weer tussen browser en server.

Filament-tabellen regelen dit zelf goed. Het risico zit in maatwerk: custom pages, widgets en eigen Livewire-componenten in je panel. Wat wij aanhouden:

  • Alleen ID's en scalars in public properties. Haal het model of de collectie op wanneer je het nodig hebt.
  • Computed properties (#[Computed] in Livewire) voor afgeleide data. Die worden niet geserialiseerd naar de browser.
  • Geen live() op elk formulierveld. Elke toetsaanslag is dan een request. Gebruik live(onBlur: true) of live(debounce: 500) waar realtime niet nodig is.

Hoe maak je dashboard-widgets snel?

Cache de zware queries en zet polling uit of lager. Stats- en chart-widgets verversen volgens de documentatie standaard elke 5 seconden.

Voor één simpele count() is dat prima. Voor omzet per maand over twee jaar orders, met tien gebruikers ingelogd, niet.

php
use App\Models\Order;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Support\Facades\Cache;

class OrderStats extends StatsOverviewWidget
{
    protected ?string $pollingInterval = null;

    protected function getStats(): array
    {
        $revenue = Cache::remember('stats.revenue.30d', now()->addMinutes(10), fn () =>
            Order::where('created_at', '>=', now()->subDays(30))->sum('grand_total')
        );

        return [
            Stat::make('Omzet 30 dagen', number_format($revenue, 2, ',', '.')),
        ];
    }
}

Widgets zijn standaard lazy-loaded: ze laden pas als ze zichtbaar zijn. Laat dat aan staan ($isLazy). Voor echt zware KPI's werkt een scheduled job die de cijfers vooraf berekent en opslaat nog beter. Meer over dashboards lees je in Filament dashboards en KPI-widgets.

Helpen Octane en filament:optimize?

php artisan filament:optimize hoort in elke productie-deploy. Octane helpt bij de vaste overhead per request, maar lost geen trage queries op.

Volgens de Filament-deploymentdocumentatie combineert filament:optimize twee commando's: filament:cache-components en icons:cache. Draai daarnaast php artisan optimize voor config- en route-caching. Controleer ook of OPcache aanstaat. Gebruik component-caching niet in development, want nieuwe componenten worden dan niet ontdekt.

Laravel Octane houdt je applicatie in het geheugen tussen requests. Elke Livewire-interactie in Filament is een volledige request, dus de bootstrap-winst telt vaak op. Maar:

  • Test je panel en je plugins op state die tussen requests blijft hangen (singletons, statische properties).
  • Octane maakt een query van 800 ms niet sneller.
  • Begin dus met queries en indexes. Octane is de laatste stap, niet de eerste.

Verwacht je pieken, bijvoorbeeld rond Black Friday? In Laravel onder hoge belasting staat hoe je de rest van de stack voorbereidt.

Hoe monitor je Filament-performance in productie?

Gebruik Laravel Pulse of Laravel Nightwatch om trage requests en queries in productie te zien. Zonder productiedata weet je niet welke tabel je gebruikers echt laat wachten.

  • Laravel Pulse is gratis, self-hosted en first-party. Het toont trage requests, trage queries en zware gebruikers in een dashboard binnen je eigen applicatie.
  • Laravel Nightwatch is de hosted monitoring van Laravel zelf, met tracing per request en alerts. Er is een gratis plan met een beperkt aantal events.

Kijk specifiek naar de Livewire-update-endpoint. Daar landen alle tabelinteracties: sorteren, filteren, pagineren.

Samengevat

  • Meet eerst. Debugbar of Telescope lokaal, preventLazyLoading() aan, Pulse of Nightwatch in productie.
  • N+1 weg: dot notation, counts(), aggregaten, modifyQueryUsing() met with().
  • Indexes op standaard sortering, filters, tabs en foreign keys. Samengesteld waar je combineert.
  • Paginering: PaginationMode::Simple of PaginationMode::Cursor bij grote tabellen, en 'all' eruit.
  • deferLoading() voor betere waargenomen snelheid. Filters deferred laten.
  • Zoeken: Scout via searchUsing(), of minder searchable kolommen.
  • Widgets: polling uit of omlaag, zware queries cachen.
  • Deploy: filament:optimize, optimize, OPcache. Octane pas als laatste stap.
  • Upgrade naar v4/v5 voor de rendering-winst, maar verwacht er geen wonderen van bij trage queries.

Veelgestelde vragen

Kan Filament overweg met een miljoen records?

Ja. Filament genereert gewone Eloquent-queries, dus de grens ligt bij je database en je indexes. Met cursor pagination, goede indexes en Scout voor zoeken blijft een tabel met een miljoen rijen bruikbaar. Zonder die ingrepen wordt al vanaf een paar honderdduizend rijen elke pagina traag.

Wat is het verschil tussen simple en cursor pagination in Filament?

Beide slaan de COUNT()-query over, dus je ziet geen totaal aantal. Simple pagination gebruikt nog OFFSET en wordt trager op diepe pagina's. Cursor pagination werkt met een cursor op een unieke kolom en blijft constant snel. Je stelt beide in via paginationMode() met de enum Filament\Tables\Enums\PaginationMode.

Maakt deferLoading() mijn tabel sneller?

Niet de query zelf. deferLoading() laadt de tabeldata asynchroon, zodat de pagina direct verschijnt en de rijen daarna volgen. Dat voelt sneller voor de gebruiker. Een trage query los je op met indexes, eager loading en een andere paginatie-modus.

Waarom is zoeken in mijn Filament-tabel zo traag?

Filament zoekt standaard met LIKE '%term%' over alle searchable kolommen. Een leading wildcard kan geen gewone index gebruiken, dus de database scant de hele tabel. Beperk het aantal searchable kolommen of koppel Laravel Scout via searchUsing().

Is upgraden naar Filament 5 genoeg voor betere performance?

Het helpt bij rendering: volgens Filament renderen grote tabellen in v4 2 tot 3 keer sneller dan in v3, en v4.12/v5.7 voegde daar meer winst aan toe. Trage database-queries lost een upgrade niet op. Combineer de upgrade dus altijd met een query-audit.

Moet ik Laravel Octane gebruiken voor Filament?

Alleen als je queries al op orde zijn en de vaste overhead per request het probleem is. Octane verlaagt de bootstrap-tijd, wat bij veel Livewire-requests oploopt. Test wel goed op gedeelde state tussen requests, vooral bij plugins van derden.

Loopt jouw Filament-panel vast op grote datasets? Neem contact op, dan kijken we samen waar de tijd zit.

Ruthger Idema

Geschreven door Ruthger Idema

15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.

Meer over ons team →
Deel dit artikel:

Wil je jouw e-commerce naar het volgende niveau?

Plan een vrijblijvend gesprek met onze experts over Magento, Shopify of Laravel maatwerk.

Plan een Tech Check