Why Initial Page Weight Kills Perceived Performance
Every Livewire component mounted on a page adds a JSON snapshot to the initial HTML payload. For dashboards with a dozen widgets, that snapshot cost compounds quickly. Livewire v3 ships three complementary tools to defer that cost: lazy components, islands, and the wire:init / #[Lazy] deferred-loading pattern. They are not interchangeable — knowing which to reach for is the real skill.
Lazy Components with #[Lazy]
Adding the #[Lazy] attribute to a component class tells Livewire to skip the full mount during the initial render and instead emit a lightweight placeholder. A subsequent HTTP request hydrates the real component.
<?php
namespace App\Livewire\Dashboard;
use Livewire\Attributes\Lazy;
use Livewire\Component;
#[Lazy]
class RevenueChart extends Component
{
public array $series = [];
public function mount(): void
{
// Runs only on the deferred request, not on initial page load.
$this->series = RevenueQuery::forCurrentMonth();
}
public function render()
{
return view('livewire.dashboard.revenue-chart');
}
}
The placeholder is whatever your placeholder() method or a placeholder.blade.php sibling view returns:
public function placeholder()
{
return <<<'BLADE'
<div class="animate-pulse h-48 bg-gray-100 rounded-xl"></div>
BLADE;
}
Isolating Lazy Requests
By default, all lazy components on a page are batched into a single deferred request. If one component is slow, it blocks the others. Pass isolate: true to give a component its own request:
#[Lazy(isolate: true)]
class SlowExternalFeed extends Component { /* ... */ }
Use isolate: true only when a component's data source is genuinely independent and slow. Batching is cheaper for components that finish quickly.
wire:init for Inline Deferred Calls
wire:init fires a component action immediately after the component is rendered in the browser. It is lighter than #[Lazy] because the component is mounted on the server — only the data-fetching action is deferred.
<div wire:init="loadStats">
@if($loaded)
<x-stats-grid :stats="$stats" />
@else
<x-skeleton-grid />
@endif
</div>
public bool $loaded = false;
public array $stats = [];
public function loadStats(): void
{
$this->stats = StatsService::summary();
$this->loaded = true;
}
This pattern is ideal when you need the component's reactive properties available immediately (e.g., for a filter bar) but want to defer the expensive query.
Islands: Truly Independent Component Trees
Livewire v3 islands are components that opt out of the parent component's update cycle. They do not re-render when an ancestor updates, making them perfect for static-ish widgets embedded inside highly reactive pages.
Declare an island by extending Livewire\Component normally — the "island" behaviour comes from how you embed it:
{{-- Inside a parent Livewire blade view --}}
@livewire('dashboard.activity-feed', lazy: true)
Combining lazy: true on the embed with #[Lazy] on the class gives you both deferred loading and snapshot isolation in one shot.
Pitfall: Islands still share the same Livewire JS bundle. They are not micro-frontends. If you need full JS isolation, that is a different architecture entirely.
Choosing the Right Tool
| Scenario | Tool |
|---|---|
| Heavy query, no interactivity until loaded | #[Lazy] |
| Component needs reactive state immediately | wire:init |
| Widget must not re-render with parent | Island + lazy: true |
| Multiple slow widgets, independent sources | #[Lazy(isolate: true)] |
Key Takeaways
#[Lazy]defers the entire mount;wire:initdefers only a method call after mount.- Batch lazy requests by default; use
isolate: trueonly for genuinely slow, independent components. - Islands prevent ancestor re-renders from cascading into a widget — combine with lazy loading for maximum effect.
- Always provide a meaningful placeholder; a blank flash is worse UX than a skeleton.
- Profile with browser DevTools network tab: look for the
livewire/updateXHR calls to confirm batching behaviour.