Key takeaways
- Islands (
@island ... @endisland) let part of a Livewire component re-render on its own, without splitting it into child components. - In Filament, they help most on custom pages and custom widget views that have several expensive, independent sections.
- They add little to stock resources, tables and forms, which Filament already renders, or to widgets, which are already separate Livewire components with lazy loading and polling.
- Watch the limits: islands can’t sit inside
@if/@foreach, can’t see outside template variables, and concurrent requests can race on shared state.
Livewire 4 arrived with a lot of excitement about islands, and Filament v5 exists partly so your panel can use Livewire 4. It’s tempting to wrap everything in @island and expect a faster admin. It won’t work out that way. This article shows where islands make a real difference in a Filament v5 panel and where they add complexity for nothing.
If you’re new to the concept, start with our overview of Livewire 4’s islands architecture. What follows assumes you know the basics.
A 60-second refresher
From the Livewire docs: “Islands allow you to create isolated regions within a Livewire component that update independently. When an action occurs inside an island, only that island re-renders, not the entire component.”
The parts you’ll use most:
@island {{-- re-renders on its own --}}
@island(lazy: true) {{-- loads when scrolled into view --}}
@island(defer: true) {{-- loads right after the page loads --}}
@island(name: 'revenue') {{-- can be targeted from outside --}}
@island(skip: true) {{-- renders only when triggered --}}
@island(always: true) {{-- also re-renders with the parent --}}To target an island from outside it, use wire:island="name" next to an action directive, or $wire.$island('name') from Alpine. wire:island.append and .prepend add content instead of replacing it.
How Filament already isolates rendering
Before adding islands, it helps to know what Filament already does for you:
| Filament building block | Already isolated? | How |
|---|---|---|
| Dashboard widgets | Yes | Each widget is its own Livewire component, lazy-loaded by default ($isLazy), with its own $pollingInterval |
| Relation managers | Yes | Separate Livewire components on the edit/view page |
| Resource tables and forms | Partly | One component per page, and Filament manages rendering internally |
| Custom pages | No | One component. Everything in its Blade re-renders on every action |
| Custom widget views | No | Every section in the widget’s view re-renders together |
That table is the whole argument. Islands are useful where Filament gives you one component with a lot inside it: custom pages and custom widget views.
Where islands help
1. A custom report page with several slow sections
This is the classic case: a “Reports” page with a revenue summary, a slow aggregate query and an external API call. Without islands, clicking “Refresh” on one section re-runs everything. With islands, each section loads and refreshes independently.
<?php
namespace App\Filament\Pages;
use App\Services\StripeReporting;
use BackedEnum;
use Filament\Pages\Page;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Computed;
class Reports extends Page
{
protected static string | BackedEnum | null $navigationIcon = Heroicon::OutlinedChartBar;
protected string $view = 'filament.pages.reports';
#[Computed]
public function revenueByRegion(): array
{
return DB::table('orders')
->selectRaw('region, SUM(total) as revenue')
->where('created_at', '>=', now()->subDays(30))
->groupBy('region')
->orderByDesc('revenue')
->get()
->all();
}
#[Computed]
public function payoutForecast(): array
{
// Slow third-party call: a good candidate for a lazy island
return app(StripeReporting::class)->forecastNextPayouts();
}
}{{-- resources/views/filament/pages/reports.blade.php --}}
<x-filament-panels::page>
@island(name: 'regions')
<x-filament::section heading="Revenue by region (30 days)">
<ul >
@foreach ($this->revenueByRegion as $row)
<li >
<span>{{ $row->region }}</span>
<span >{{ Number::currency($row->revenue) }}</span>
</li>
@endforeach
</ul>
<x-filament::button size="sm" color="gray" wire:click="$refresh">
Refresh
</x-filament::button>
</x-filament::section>
@endisland
@island(name: 'payouts', lazy: true)
@placeholder
<x-filament::section heading="Payout forecast">
<x-filament::loading-indicator />
</x-filament::section>
@endplaceholder
<x-filament::section heading="Payout forecast">
{{-- render $this->payoutForecast --}}
</x-filament::section>
@endisland
</x-filament-panels::page>What you get:
- The page shell appears immediately. The Stripe call happens in a separate request once the section scrolls into view.
- “Refresh” inside the
regionsisland re-runs onlyrevenueByRegion, because computed properties evaluate lazily, when a render needs them.
This needs Filament v5 (the protected string $view property is the v4/v5 style). For tidiness, keep @island blocks directly in the page’s own Blade file rather than in partials.
2. A button in the page header that refreshes one section
Named islands can be refreshed from anywhere in the component:
<x-filament::button wire:click="$refresh" wire:island="regions" icon="heroicon-m-arrow-path">
Refresh regions
</x-filament::button>If you give two islands the same name, they render as a group. For example, a KPI in the page header and the same KPI in a sidebar card will stay in sync.
3. Activity feeds and audit timelines with “Load more”
Append mode suits a custom “Activity” page, or a timeline under an audit log:
public int $page = 1;
public function loadMore(): void
{
$this->page++;
}
#[Computed]
public function activities()
{
return \Spatie\Activitylog\Models\Activity::query()
->with('causer')
->latest()
->forPage($this->page, 25)
->get();
}@island(name: 'feed')
@foreach ($this->activities as $activity)
<div wire:key="activity-{{ $activity->id }}" >
<strong>{{ $activity->causer?->name ?? 'System' }}</strong>
{{ $activity->description }}
<span >{{ $activity->created_at->diffForHumans() }}</span>
</div>
@endforeach
@endisland
<x-filament::button wire:click="loadMore" wire:island.append="feed">
Load more
</x-filament::button>Each click renders only the next 25 rows and appends them. Nothing else on the page re-renders.
4. Expensive content that only some users need
skip: true renders a placeholder, often a button, and only runs the island when someone asks for it. That works well for “Show full breakdown” panels on a record’s custom page:
@island(skip: true)
@placeholder
<x-filament::button color="gray" wire:click="$refresh">
Load cohort breakdown
</x-filament::button>
@endplaceholder
@include('filament.partials.cohort-table', ['rows' => $this->cohorts])
@endisland5. Polling one small region
wire:poll inside an island refreshes only that island. On a custom operations page, poll the “queue depth” badge every five seconds and leave the rest of the page alone:
@island
<div wire:poll.5s>
Jobs waiting: {{ $this->queueDepth }}
</div>
@endislandLivewire 4 polling is also non-blocking, so it won’t hold up a user’s click elsewhere on the page.
Where islands don’t help (and what to use instead)
Standard resource pages. List, create and edit pages are built from Filament’s schema and table components, and you don’t write their Blade. There’s nothing sensible to wrap. If a list page is slow, fix the query. Our 8 fixes for slow Filament tables covers eager loading, pagination modes and search indexes.
Dashboards made of standard widgets. Each StatsOverviewWidget, ChartWidget and TableWidget is already its own component, lazy by default, with its own polling interval. Islands add nothing here. Tune $pollingInterval instead, as covered in our real-time dashboard widgets guide.
Tightly coupled UI. If section B depends on a filter chosen in section A, putting them in separate islands means fighting the design. Either keep them together, or use always: true so B re-renders whenever the parent does.
Content inside loops or conditionals. Islands can’t be declared inside @foreach or @if. Put the loop or condition inside the island instead.
Islands vs the other Livewire 4 tools
| You need… | Use |
|---|---|
| A section of one component to refresh on its own | @island |
| A slow section to load after the page | @island(lazy: true) or @island(defer: true) |
| A reusable piece with its own state and lifecycle | A child Livewire component (or a Filament widget) |
| A whole component to load after the page | <livewire:foo defer /> or #[Defer] |
| A call that shouldn’t block or re-render | wire:click.async / #[Async] / .renderless |
Gotchas to know before shipping
- Data scope. Islands can read component properties and methods (
$this->revenue), but not template variables defined outside them. An@php $x = ... @endphpabove the island isn’t visible inside it. - Racing requests. Island requests run in parallel. If two islands change the same public property, the response that arrives last wins. Keep writes in one place.
- Computed caching. Use
#[Computed]for anything expensive, so it only runs when an island that needs it renders. - Filament’s Blade components are fine inside islands.
x-filament::section,x-filament::buttonand the loading indicator all work. Filament schemas (forms, infolists) belong to the component’s lifecycle, though, so don’t wrap{{ $this->form }}in an island and expect a form that validates separately. - Test it. Livewire’s testing helpers exercise the component as a whole, so add a browser test (Pest 4 browser testing works well) for any flow that depends on island behaviour.
FAQ
Do I need Filament v5 to use Livewire islands?
Yes. Islands are a Livewire 4 feature, and Filament v5 is the version that runs on Livewire 4. Filament v4 uses Livewire 3.
Will islands make my Filament tables faster?
Not really. Table performance depends on the query, pagination and columns. Islands help custom pages and custom widget views that have several independent sections.
What’s the difference between a lazy island and a lazy widget?
A lazy widget is a separate Livewire component with its own state. A lazy island is a region inside one component that shares that component’s state. Use widgets for reusable dashboard pieces and islands for sections within a single custom page.
Can I trigger an island from a Filament action?
Filament actions run on the component and re-render it. To refresh just one island from your own button, use wire:island="name" with wire:click, or $wire.$island('name').$refresh() from Alpine.
Sources and further reading
- Livewire 4 islands documentation
- @island directive reference
- Livewire lazy and deferred loading
- Filament widgets overview
- Filament custom pages
Related on The Web Tier: Livewire 4’s islands architecture · Filament v5 vs v4
