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 Islands and Lazy Loading Matter

A Livewire page that boots five components on the initial request pays the full PHP hydration cost upfront — database queries, authorization checks, and serialization all happen before the first byte reaches the browser. Livewire v3 ships three complementary tools to break that cost apart: lazy components, deferred properties, and the island pattern (composing independent component trees). Used together they let you render a fast, mostly-static shell and hydrate interactive regions only when the browser is ready.


Lazy Components: Hydrate After Paint

Adding #[Lazy] to a component class tells Livewire to render a placeholder on the server and fire a subsequent network request to hydrate the real component after the page loads.

<?php

namespace App\Livewire;

use Livewire\Attributes\Lazy;
use Livewire\Component;

#[Lazy]
class RevenueChart extends Component
{
    public function render()
    {
        return view('livewire.revenue-chart', [
            'data' => $this->buildChartData(), // expensive query
        ]);
    }
}

The placeholder is whatever your placeholder() method (or a placeholder.blade.php sibling view) returns:

public function placeholder(array $params = []): \Illuminate\View\View
{
    return view('livewire.placeholders.chart-skeleton');
}

Pitfall: #[Lazy] fires a full Livewire request per component. If you have ten lazy components on one page you get ten extra HTTP round-trips. Batch related data into a single component or use #[Lazy(isolate: false)] to merge all lazy requests on a page into one.

#[Lazy(isolate: false)]
class RevenueChart extends Component { /* ... */ }

With isolate: false, Livewire groups all non-isolated lazy components into a single batched request — a significant win on dashboard pages.


Deferred Properties

Sometimes you want the component to hydrate immediately but defer loading a specific expensive property. The #[Deferred] attribute (available from Livewire 3.4+) does exactly that:

use Livewire\Attributes\Deferred;

class OrdersTable extends Component
{
    #[Deferred]
    public array $summary = [];

    public function mount(): void
    {
        // $this->summary is NOT populated here on first render
    }

    public function loadSummary(): void
    {
        $this->summary = Order::query()->selectRaw('...')->get()->toArray();
    }
}

In the Blade template you trigger the load via a lifecycle hook or a user action, keeping the initial render cheap.


The Island Pattern: Independent Component Trees

An "island" is simply a Livewire component that owns its own state and does not share Livewire wire-model bindings with its parent. The practical benefit is isolation: re-rendering one island does not trigger a diff on sibling islands.

{{-- resources/views/dashboard.blade.php --}}
<div class="grid grid-cols-3 gap-6">
    <livewire:stats-card :key="'stats'" />
    <livewire:activity-feed :key="'feed'" />
    <livewire:quick-actions :key="'actions'" />
</div>

Always provide explicit :key values. Without them Livewire may morph the wrong DOM node when the parent re-renders, causing flicker or lost state.

Combine islands with #[Lazy(isolate: false)] for a dashboard that renders a static shell instantly and hydrates all islands in one batched request:

#[Lazy(isolate: false)]
class StatsCard extends Component { /* ... */ }

#[Lazy(isolate: false)]
class ActivityFeed extends Component { /* ... */ }

#[Lazy(isolate: false)]
class QuickActions extends Component { /* ... */ }

Skeleton Placeholders Without Extra Views

For quick prototyping you can return an inline placeholder without a dedicated view file:

public function placeholder(): string
{
    return <<<'HTML'
    <div class="animate-pulse h-48 bg-gray-100 rounded-xl"></div>
    HTML;
}

This keeps the component self-contained and avoids view proliferation.


Key Takeaways

  • Use #[Lazy] to defer expensive component hydration until after first paint.
  • Set isolate: false on dashboards to batch all lazy requests into one HTTP call.
  • #[Deferred] properties let a component hydrate cheaply and load heavy data on demand.
  • Explicit :key bindings on island components prevent morph collisions.
  • Measure with browser DevTools Network tab — count Livewire requests before and after to validate gains.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Does #[Lazy] work with Livewire components that receive props from the parent?
Yes. Props passed via the component tag (e.g., :userId="$id") are serialized into the placeholder request and forwarded to mount() when the lazy hydration request fires. Just ensure the props are serializable — avoid passing Eloquent models directly; pass IDs instead.
Q02 What is the difference between #[Lazy] and #[Deferred] in Livewire v3?
#[Lazy] defers the entire component hydration — the component renders a placeholder and boots on a subsequent request. #[Deferred] hydrates the component normally but skips populating a specific property on the first render, letting you load it lazily via a method call or lifecycle hook.
Q03 Can I use lazy components inside Filament panels?
Filament widgets are Livewire components under the hood, so #[Lazy] applies. However, Filament's own lazy widget mechanism (the $isLazy property on widgets) is the preferred approach inside panels, as it integrates with Filament's rendering pipeline and avoids conflicts with panel layout hydration.

Continue reading

More Articles

View all