Livewire v3 Islands, Lazy Components, and Deferred Loading in Practice
#livewire #laravel #performance #frontend

Livewire v3 Islands, Lazy Components, and Deferred Loading in Practice

1 min read Mohamed Said Mohamed Said

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:model or 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 custom placeholder() to ship skeleton UIs at zero query cost.
  • Lazy components are isolated — do not expect them to react to parent property changes automatically.
  • wire:init is 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.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Does a lazy component re-render when its parent's state changes?
No. In Livewire v3, lazy components are isolated by default. They do not automatically re-render when parent properties change. Use events via dispatch/on or remove the #[Lazy] attribute and rely on wire:key-driven re-mounting if you need reactivity.
Q02 When should I use #[Lazy] versus wire:init?
#[Lazy] renders a placeholder immediately and hydrates the entire component in a follow-up request — ideal when the component itself is expensive to boot. wire:init mounts the component normally but defers a specific method call, making it better when you need the component shell interactive before the data arrives.
Q03 Are there side-effect risks with lazy components?
Yes. mount() runs on both the placeholder render and the real hydration render. Any side effects in mount() — logging, counter increments, external API calls — will fire twice. Move side effects to a method called via wire:init or a post-hydration lifecycle hook.

Continue reading

More Articles

View all