Slow Filament screens are usually blamed on the wrong layer. Teams upgrade packages, swap columns, or remove filters before they know whether the real cost is database work, Livewire hydration, authorization checks, or table configuration. That is how you waste a week and still keep a slow screen.
The better approach is simple: benchmark one screen, break the cost into parts, then change the part that is actually expensive. Filament sits on top of Laravel and Livewire, so a slow admin page is rarely a single-problem system. You need a workflow that makes the bottleneck obvious before you touch the UI.
This tutorial is that workflow. It is practical, repeatable, and biased toward real codebases where the screen already exists and users already think it feels slow.
Start with one repeatable benchmark
Do not begin with your whole panel. Pick one screen that people actually complain about: a resource index with heavy filters, an edit page with relation managers, or a dashboard that loads too many widgets at once.
Then make the benchmark repeatable:
- use the same local database snapshot each run
- test with a realistic row count, not ten seed records
- measure a cold page load and one warm interaction
- close unrelated browser tabs and disable noisy extensions
- write the numbers down before changing code
For most Filament screens, I want three checkpoints:
- initial page load
- a common interaction like search, filter, or sort
- opening a row action or modal
If you do not measure those separately, you will blur together very different costs.
What to capture
At minimum, record these numbers for each checkpoint:
- total request time
- SQL query count
- total SQL time
- response payload size
- number of Livewire requests triggered
That already tells you a lot. A page with 220 queries and 900 ms of SQL time is a database problem until proven otherwise. A page with modest SQL time but huge payloads and repeated requests is usually a Livewire or component-state problem. A page with acceptable backend timings but sluggish interactions may be rendering too much UI or doing too much per row.
A lightweight way to start is adding Server-Timing and query metrics to local requests:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
class ProfileAdminRequest
{
public function handle(Request $request, Closure $next): Response
{
if (! str_starts_with($request->path(), 'admin')) {
return $next($request);
}
$queryCount = 0;
$queryTimeMs = 0;
$startedAt = hrtime(true);
DB::listen(function (QueryExecuted $query) use (&$queryCount, &$queryTimeMs) {
$queryCount++;
$queryTimeMs += $query->time;
});
/** @var Response $response */
$response = $next($request);
$totalMs = (hrtime(true) - $startedAt) / 1_000_000;
$response->headers->set(
'Server-Timing',
sprintf('app;dur=%.1f, sql;dur=%.1f, queries;desc="%d"', $totalMs, $queryTimeMs, $queryCount)
);
return $response;
}
}
Now Chrome DevTools gives you backend timing hints without turning every request into a detective story.
If you want richer local visibility, Laravel Telescope is still one of the most useful ways to inspect queries, requests, jobs, and exceptions in one place. For Filament-specific debugging, it is often enough.
Add instrumentation where Filament actually spends time
A Filament admin page usually burns time in four places:
- database queries
- per-record authorization or visibility logic
- Livewire hydration and payload size
- table or form configuration that does more work than the user needs
That means your benchmark has to go beyond total duration.
Database timing first
Laravel already gives you enough primitives to catch obvious waste. In addition to DB::listen, use whenQueryingForLongerThan in local or staging to flag suspicious requests early:
<?php
namespace App\Providers;
use Illuminate\Database\Connection;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Log;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
DB::whenQueryingForLongerThan(250, function (Connection $connection, QueryExecuted $event) {
Log::warning('Slow query threshold exceeded', [
'sql' => $event->sql,
'time_ms' => $event->time,
'connection' => $connection->getName(),
]);
});
}
}
That will not replace profiling, but it quickly exposes screens where a table interaction is quietly executing a pile of avoidable work.
Watch request shape, not just runtime
Livewire pages can feel slow even when SQL looks fine. Two common reasons are oversized component state and too many requests for small UI changes.
Livewire’s current docs explicitly warn that storing large Eloquent collections as component properties can hurt performance because hydration re-executes work on subsequent requests. That is why a Filament page with "helpful" preloaded collections often degrades over time instead of getting better.
Also watch how many requests a single interaction triggers. Search fields, reactive filters, dependent selects, and polling widgets can stack together in ugly ways.
Separate initial render from interaction cost
This is where teams often misdiagnose the problem.
A screen may load fine at first, then feel terrible when users search or change filters. That usually points to table search, per-row closures, or repeated Livewire updates. Filament’s table docs note that global search term splitting can hurt performance on large datasets, and that is exactly the kind of detail that matters only after you have measured the slow interaction path.
So benchmark both:
- first render of the page
- first search request
- first filter request
- first modal open
Do not assume the same fix helps all four.
Read the usual bottlenecks correctly
Once you have timings, the next step is classification. Most slow Filament screens fall into a few predictable buckets.
Database-heavy tables
This is the classic case: relation columns, badge counts, computed summaries, and searchable fields all pile onto one index page. The query count spikes, SQL time dominates the request, and every sort or filter makes it worse.
Typical causes:
- missing eager loading for related columns
- per-row
count()or aggregate calls inside closures - global search across too many text columns
- filters that build expensive subqueries
-
options()lists loading huge tables on every request
This is where Filament table configuration matters more than package upgrades. If a table is asking the database to do the wrong work, a faster Filament release will only make the wrong work happen a bit more efficiently.
Authorization-heavy rows and actions
Filament makes it easy to add visible(), hidden(), disabled(), and policy-based actions everywhere. That is good ergonomically and dangerous operationally.
If each row action or badge checks permissions through expensive closures, you can end up paying authorization cost hundreds of times on one table render. The page still looks like a "UI problem," but the real issue is repeated decision logic.
A useful smell test is this: if reducing page size from 50 records to 10 dramatically improves the screen even when SQL is already reasonable, per-row logic is probably part of the problem.
Livewire state and request churn
A different pattern is moderate SQL time but clumsy interactions. Search feels sticky. Filter changes trigger multiple requests. Opening a modal rehydrates more state than expected.
That usually means too much data lives in the component, too many fields are reactive by default, or several components are bundled into one slow request path.
Livewire gives you tools here, but they are not magic. #[Isolate] helps when one component is expensive and independent. Lazy loading helps when widgets do not need to block first paint. Computed properties help when you were previously carrying around heavy collections as public state. The point is to shrink work per interaction, not to sprinkle attributes at random.
Production-only slowness
If local numbers look acceptable and production feels worse, stop tuning the screen first and check deployment posture.
Filament’s deployment docs recommend php artisan filament:optimize, which wraps component and icon caching, and Laravel still benefits from the usual production optimizations like php artisan optimize and correct OPcache setup. Those are not substitutes for bad screen design, but they absolutely matter once the screen is otherwise healthy.
Fix the bottleneck you measured
This is the part that should feel boring. Good performance work is usually a series of targeted, unsurprising fixes.
Tighten the table query before touching the UI
If the benchmark says the table query is the bottleneck, change the query first:
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
public static function table(Table $table): Table
{
return $table
->modifyQueryUsing(fn (Builder $query) => $query
->select(['id', 'customer_id', 'status', 'total', 'created_at'])
->with(['customer:id,name'])
->withCount('items'))
->columns([
Tables\Columns\TextColumn::make('customer.name')->searchable(),
Tables\Columns\TextColumn::make('items_count'),
Tables\Columns\TextColumn::make('status')->badge(),
Tables\Columns\TextColumn::make('total')->money('USD'),
Tables\Columns\TextColumn::make('created_at')->dateTime(),
])
->defaultPaginationPageOption(25)
->splitSearchTerms(false);
}
Three things are happening here:
- the base select is narrower
- relations and counts are loaded intentionally
- search behavior is made cheaper for large datasets
That is a much better starting point than deleting columns until the page stops hurting.
Stop doing expensive work per row
If per-record closures are the issue, move repeated logic up a level.
Bad pattern:
- action visibility closures that hit services or policies for every record
- badge or description closures that trigger relation access lazily
- ad hoc formatting that performs database lookups inside the column callback
Better pattern:
- preload the data the UI needs
- compute coarse permissions once per request where possible
- reserve per-row checks for cases that are truly record-specific
This is also where reducing visual ambition helps. Admin tables do not need to be miniature dashboards. If each row shows three badges, two counts, a derived status, and four action buttons, you are paying for that complexity every render.
Shrink Livewire work on interaction
When interactions are the main problem, focus on state and request frequency.
Useful moves:
- replace large public collections with computed lookups or paginated queries
- avoid making fields reactive unless the user genuinely needs instant feedback
- lazy-load widgets that are not needed for first paint
- isolate an expensive widget only if it does not need to coordinate with the rest of the page
Be careful here: request bundling in Livewire often helps overall performance. Isolating components is for targeted cases, not a default style.
Use upgrades as a measured final step
Filament and Livewire releases do ship real performance work. Newer versions have improved rendering paths, component handling, and request behavior. But the right order is still:
- measure the screen
- fix obvious query and state issues
- upgrade if the version gap is meaningful
- rerun the same benchmark
Otherwise you never learn whether the upgrade solved your problem or merely shifted it.
Turn one benchmark into a guardrail
A single successful tuning pass is useful. A repeatable performance habit is better.
Once you make one Filament screen faster, keep the process:
- store the before and after timings in the PR description
- keep one realistic local dataset for admin profiling
- add a short checklist for new heavy resources
- rerun the same benchmark after package upgrades
A checklist can be brutally small:
- does the table eager load displayed relations?
- are counts and aggregates precomputed sensibly?
- is search scoped narrowly enough?
- are actions or badges doing expensive per-row work?
- is Livewire carrying more state than the screen needs?
That is enough to stop most regressions.
If you want useful official references while working through this, keep these close:
- Filament table docs: filamentphp.com/docs/5.x/tables/overview
- Filament deployment and optimization: filamentphp.com/docs/4.x/deployment
- Livewire properties and hydration guidance: livewire.laravel.com/docs/4.x/properties
- Livewire isolation for expensive independent components: livewire.laravel.com/docs/4.x/attribute-isolate
- Laravel Telescope: laravel.com/docs/13.x/telescope
The practical rule is simple: do not optimize Filament screens by taste. Measure one screen, separate SQL from hydration from UI logic, fix the dominant cost, and rerun the same benchmark. That is how you make an admin panel faster without guessing.
Read the full post on QCode: https://qcode.in/benchmark-filament-admin-screens-without-guesswork/
Top comments (0)