Why Lazy Loading Matters in Livewire v3
Every Livewire component that renders on the initial request adds to your server's time-to-first-byte. For dashboards with heavy aggregations, charts, or permission-gated widgets, that cost compounds fast. Livewire v3 ships three complementary tools: lazy components, deferred updates, and the emerging islands mental model. Used together they let you serve a shell instantly and hydrate expensive pieces asynchronously.
Lazy Components: The Basics Done Right
Adding #[Lazy] to a component tells Livewire to render a placeholder on the first request and fire a subsequent network call to hydrate the real component.
<?php
namespace App\Livewire\Dashboard;
use Livewire\Attributes\Lazy;
use Livewire\Component;
#[Lazy]
class RevenueChart extends Component
{
public function render(): \Illuminate\View\View
{
return view('livewire.dashboard.revenue-chart', [
'data' => $this->buildChartData(), // expensive query
]);
}
}
By default Livewire renders an empty <div> as the placeholder. Override it with a placeholder() method to show a skeleton:
public function placeholder(array $params = []): \Illuminate\View\View
{
return view('livewire.dashboard.revenue-chart-skeleton');
}
The skeleton view is a plain Blade file — no Livewire overhead — so it renders at near-zero cost.
Isolating Lazy Components from Their Parents
A common mistake: passing reactive properties into a lazy component and expecting them to re-hydrate when the parent updates. Lazy components are isolated by default in v3 — they do not re-render when parent state changes unless you explicitly wire them.
If you need the child to react to parent changes, drop #[Lazy] and use a regular component with wire:key to force re-mount on relevant state transitions instead.
@foreach ($selectedPeriods as $period)
<livewire:dashboard.period-summary :period="$period" :key="'period-'.$period->id" />
@endforeach
Deferred Loading with wire:init
For cases where you want the component to mount fully but defer a specific data-fetch, wire:init is the right tool:
<div wire:init="loadMetrics">
@if($loaded)
<x-metrics-table :rows="$rows" />
@else
<x-skeleton-table />
@endif
</div>
public bool $loaded = false;
public array $rows = [];
public function loadMetrics(): void
{
$this->rows = Metric::forCurrentTenant()->latest()->get()->toArray();
$this->loaded = true;
}
wire:init fires immediately after the component mounts in the browser, giving you one extra round-trip but keeping the initial HTML lightweight.
The Islands Mental Model
Think of each lazy or deferred component as an island: a self-contained interactive region that hydrates independently. This mirrors the Astro/islands architecture pattern. In practice it means:
- Keep islands small and focused — one responsibility per component.
- Avoid sharing mutable state between islands through the URL or session; use explicit
wire:modelor events instead. - Use
dispatch()/on()for cross-island communication rather than parent-child property drilling.
// Island A dispatches
$this->dispatch('period-changed', period: $this->period);
// Island B listens
#[On('period-changed')]
public function handlePeriodChange(string $period): void
{
$this->period = $period;
$this->loadMetrics();
}
Avoiding the Double-Render Trap
Lazy components render twice: once for the placeholder, once for the real content. If your component has side effects in mount() (logging, incrementing counters), those run on both renders. Move side effects into a dedicated action method called from wire:init or a lifecycle hook that only fires post-hydration.
Takeaways
- Use
#[Lazy]with a customplaceholder()to ship skeleton UIs at zero query cost. - Lazy components are isolated — do not expect them to react to parent property changes automatically.
wire:initis better than#[Lazy]when you need the full component mounted but want to defer one expensive fetch.- Treat each lazy component as an island: small, self-contained, communicating via events.
- Audit
mount()for side effects — they run on both the placeholder and the real render pass.