The Cost of Convenience
I like Filament. It makes building admin panels and CRMs incredibly fast. That speed, though, can hide a problem: default Filament table queries are often inefficient for large datasets. You start with a few thousand records, everything's fine. Then you hit hundreds of thousands, or millions, and suddenly your admin panel is crawling. You're clicking a table column to sort, and it's taking five seconds. That's not just annoying; it's a productivity killer for your team.
My philosophy is simple: any interaction in an admin panel should feel instant. If it takes more than a second, it's broken. This isn't theoretical; I've shipped and operated production systems with millions of records where Filament tables perform just as fast as they did with a hundred. It takes intentionality.
N+1 Problems: The Usual Suspect
The first place I always look is N+1 queries. Filament's default table builder, especially with relationships, can easily fall into this trap. If you're displaying a related model's attribute in a table column, and you haven't explicitly eager-loaded that relationship, Filament will execute a separate query for each row to fetch that data. This is death by a thousand cuts when your table has hundreds of rows per page.
The fix is straightforward: use ->relationship() on your column definitions for sorting and filtering, and add ->preload() or ->eagerLoad() to your table query for display.
// In your Filament Table definition
Tables\Columns\TextColumn::make('author.name')
->label('Author')
->sortable()
->searchable()
->relationship('author', 'name'), // Important for sort/search on related data
// In your ListPosts.php (or wherever you define the table query)
public static function getTableQuery(): Builder
{
return Post::query()
->with('author'); // Eager load the relationship for display
}Don't just guess which relationships are causing N+1s. Use a tool like Laravel Debugbar or Clockwork to monitor your queries. Look for hundreds of duplicate queries. It's usually obvious once you see it.
Indexing: Your Best Friend
This sounds basic, but you'd be surprised how often I find missing indexes on production databases that are causing Filament slowness. If you're sorting, filtering, or searching on a column in your Filament table, that column needs an index. Period.
Laravel migrations make this easy:
Schema::table('posts', function (Blueprint $table) {
$table->index('status');
$table->index('published_at');
$table->fullText('title'); // For full-text search, if applicable
});A common mistake is forgetting indexes on foreign keys. If you have user_id on your posts table, and you're displaying the author's name, ensure user_id is indexed. The database needs to quickly look up those related records.
For text columns you're searching, consider LIKE '%value%' queries. These are notoriously slow without proper indexing or full-text search solutions. If you're using MySQL, its built-in full-text search can help. For PostgreSQL, consider pg_trgm or a dedicated search service like Algolia or MeiliSearch if your search needs are complex and high-volume. Filament integrates well with these via custom query builders.
Customizing the Query Builder
Sometimes, the default Filament query isn't enough. You might need to add complex joins, subqueries, or select only specific columns to reduce data transfer. Filament gives you full control over the query builder for your tables.
You can define getTableQuery() directly in your List page or resource:
public static function getTableQuery(): Builder
{
return Post::query()
->select(['id', 'title', 'status', 'published_at', 'user_id']) // Only fetch what's needed
->addSelect(['author_name' => User::select('name')->whereColumn('users.id', 'posts.user_id')->limit(1)]) // Example subquery
->where('status', '!=', 'draft');
}Be careful not to over-optimize here. Only customize when you have a specific performance problem you're trying to solve, and you've verified the default query is the bottleneck. Adding too much complexity can make your code harder to maintain.
Avoid Expensive Computed Columns
Filament allows you to define custom columns that compute values on the fly. While convenient, these can be performance killers if the computation is expensive and runs for every row. Imagine a column that calculates a sum of related records for each post in a table of thousands. That's a lot of extra queries or computations.
If you need a computed value, try to persist it in the database if possible (e.g., a total_comments_count column updated via a model observer). If it must be computed, consider lazy loading it or only displaying it on the detail page, not the main table. Or, if it's a simple calculation, ensure it runs directly in the database via a raw select statement or a generated column, rather than in PHP for each row.
The Paging Trap
Filament defaults to 50 items per page. For very large datasets, especially with complex queries, fetching even 50 items can be slow if the underlying query has to scan millions of rows to find the correct offset. Ensure your queries are using indexes efficiently for sorting and filtering, so the database can quickly jump to the correct page of results.
If you have extremely wide tables (many columns) and complex relationships, consider reducing the items per page to 25 or even 10. This reduces the immediate data load, but it's often a band-aid for an unoptimized query rather than a solution itself. Always fix the query first.
Final Thoughts
Optimizing Filament for large datasets isn't about magic; it's about understanding how databases work and applying basic performance principles. Eager load relationships, index your columns, and don't be afraid to take control of the query builder. Always measure before you optimize, and focus on the bottlenecks identified by real data, not just assumptions. Your users, and your team, will thank you for a snappy admin panel.


