Livewire 4 Islands — Lazy-Load Expensive Components

Use Livewire 4's island architecture to lazy-load slow dashboard sections without blocking the page. Covers @island, lazy, defer, named islands, and gotchas.

Steven Richardson
Steven Richardson
· 7 min read

You have a Livewire dashboard. One section hits the database hard — revenue aggregates, a complex report, something slow. Every page load blocks on that query, and the user stares at a spinner for the entire component until it finishes.

Before Livewire 4, your options were wire:init (fire a request on mount), child components with lazy (create a whole new component just to isolate one section), or reaching for Alpine. Livewire 4 adds a cleaner answer: islands.

What Are Livewire 4 Islands?#

Islands are isolated regions inside a Livewire component. When an action inside an island triggers a server round-trip, only that island re-renders — not the entire component. No extra component class. No prop-passing. Just a @island / @endisland wrapper in your Blade template.

If you're still on Livewire 3, check out the practical migration guide from Livewire 3 to Livewire 4 before diving in — islands are a Livewire 4-only feature.

A basic island looks like this:

<div>
    {{-- This part renders immediately with the page --}}
    <h1>Dashboard</h1>
    <p>Welcome back, {{ $user->name }}</p>

    {{-- This island renders independently --}}
    @island
        <div>
            Revenue this month: {{ $this->monthlyRevenue }}
        </div>
    @endisland
</div>

The monthlyRevenue computed property runs only when the island renders, not when the rest of the component mounts. The critical insight: computed properties referenced inside an island are scoped to that island's render cycle.

Lazy Loading — Viewport-Triggered and Deferred#

An island by itself still renders on the initial page load. The performance win comes when you add lazy or defer.

lazy: true — loads the island when it scrolls into the viewport, using an intersection observer:

@island(lazy: true)
    @placeholder
        <div class="animate-pulse h-32 bg-gray-100 rounded-lg"></div>
    @endplaceholder

    <div>
        Revenue this month: {{ $this->monthlyRevenue }}
    </div>
@endisland

The @placeholder / @endplaceholder block renders immediately in place of the island while it's loading. Use it for skeleton loaders, spinners, or a rough layout placeholder — anything that keeps the page feeling responsive.

defer: true — loads the island immediately after the page has finished painting, regardless of scroll position:

@island(defer: true)
    <div>
        <canvas id="revenue-chart"></canvas>
        {{-- Chart data populated by a computed property --}}
    </div>
@endisland

Prefer defer for above-the-fold content you want to load quickly but not block the initial render. Use lazy for below-the-fold sections the user might never scroll to.

Named Islands — Targeted Refreshes#

Sometimes you want to refresh a specific island from a button that lives outside the island. Name it:

<div>
    {{-- Button lives outside the island --}}
    <button wire:click="$refresh" wire:island="revenue">
        Refresh Revenue
    </button>

    @island(name: 'revenue')
        <div>
            Revenue this month: {{ $this->monthlyRevenue }}
        </div>
    @endisland
</div>

wire:island="revenue" on the button tells Livewire to only re-render the revenue island when that click fires, not the whole component. The naming also works with wire:poll:

@island(name: 'job-status', lazy: true)
    @placeholder
        <div class="animate-pulse h-8 bg-gray-100 rounded"></div>
    @endplaceholder

    <div wire:poll.5s>
        Job status: {{ $this->currentJobStatus }}
    </div>
@endisland

That wire:poll.5s only triggers re-renders of job-status every five seconds — not the whole component. This is a significant improvement over putting wire:poll on a container element in Livewire 3.

Lazy vs wire:init vs Lazy-Loaded Child Components#

Here's where things were previously confusing. Three approaches did similar things in Livewire 3:

wire:init="loadData" dispatched a Livewire call after mount to fetch data and re-render the component. The whole component re-rendered. It also caused a full round-trip on every subsequent interaction.

Lazy-loaded child components (using the #[Lazy] attribute in Livewire 3) created a separate component class just to isolate the slow part. It worked, but you now had props to pass, events to emit, and another file to maintain.

Livewire 4 islands give you the isolation of a child component without the overhead. The parent component class stays in one place. No props, no events. The trade-off: you lose the ability to use islands inside loops or conditionals (more on that below).

For truly independent sections that need their own state, lifecycle hooks, or complex interactions — use a proper child component. For slow sections that are really just read-heavy parts of the same screen, islands are cleaner.

Performance Wins#

The gains compound as your component gets more complex. Consider a dashboard with three expensive sections:

<div>
    <h1>Dashboard</h1>

    @island(lazy: true)
        @placeholder
            <div class="animate-pulse h-48 bg-gray-100 rounded-lg mb-4"></div>
        @endplaceholder
        <div>{{ $this->revenueMetrics }}</div>
    @endisland

    @island(defer: true)
        <div>{{ $this->activeSubscriptions }}</div>
    @endisland

    @island(lazy: true)
        @placeholder
            <div class="animate-pulse h-64 bg-gray-100 rounded-lg"></div>
        @endplaceholder
        <div>{{ $this->churnReport }}</div>
    @endisland
</div>

Without islands, the component waits for all three queries before painting anything. With islands, the page frame renders immediately, activeSubscriptions loads after page paint, and the other two load as the user scrolls. Each island only runs its own computed properties when it renders.

When you're profiling before and after, Laravel Telescope, Debugbar, and Pulse all show island requests as separate round-trips — handy for confirming query isolation is actually working.

Gotchas and Edge Cases#

Islands can't go inside loops or conditionals. This is the biggest limitation:

{{-- ❌ This won't work --}}
@foreach ($users as $user)
    @island
        {{ $user->expensiveMetric }}
    @endisland
@endforeach

{{-- ✅ Put the loop inside the island instead --}}
@island
    @foreach ($this->users as $user)
        {{ $user->expensiveMetric }}
    @endforeach
@endisland

Note that inside an island, you reference properties via $this-> rather than Blade's loop variables.

Islands can't access parent loop variables. Since an island has no access to @foreach context, any data it needs must come from the component's properties or computed properties.

Each island is a separate network request. Three lazy islands on a page means up to three extra round-trips when they trigger. For sections where the data is cheap, a plain computed property (no island) is the right call.

wire:model changed in Livewire 4. It now only listens to events from the element itself, not bubbling events from children. This catches some developers out when they have wire:model on container elements inside an island. If inputs inside your island aren't binding correctly, check whether they're firing directly on the bound element.

For real-time features that need server push rather than polling, Laravel Reverb with Echo is the complement to islands — Reverb pushes events from the server, islands isolate the regions that react to them.

Wrapping Up#

Islands are the right tool for slow, read-heavy sections of a Livewire component that you want to load independently. The API is small — @island, lazy, defer, name, @placeholder — and the performance gains on complex dashboards are real.

For the underlying component patterns and what else changed in Livewire 4, the full Livewire 3 to 4 migration guide covers breaking changes and the new architecture in detail. If you're offloading truly heavy processing to the background rather than deferring rendering, Laravel queues in production is the next step.

FAQ#

What are Livewire 4 islands?

Islands are isolated regions inside a Livewire component, defined with the @island / @endisland Blade directives. When something inside an island triggers a server request, only that island re-renders rather than the whole component. They were introduced in Livewire 4 (released January 2026) as a built-in way to scope expensive sections without creating separate child component classes.

How do I lazy-load a Livewire component?

In Livewire 4, wrap the expensive section in @island(lazy: true). The island renders a placeholder immediately and loads the real content when it scrolls into the viewport. Use @island(defer: true) instead if you want it to load immediately after the page paints regardless of scroll position. For Livewire 3, the #[Lazy] attribute on a child component class provided similar behaviour.

How do I improve Livewire performance?

Islands are one lever — isolate expensive computed properties behind @island(lazy: true) or @island(defer: true) so they don't block the initial render. Beyond islands, Livewire 4's "Blaze" pre-rendering layer compiles static Blade template sections at build time, reducing per-request work significantly. At the infrastructure level, caching computed properties with #[Computed(cache: true)] and offloading heavy work to queued jobs also helps.

Can I use islands inside a foreach loop in Livewire 4?

No — @island cannot be placed inside @foreach, @if, or other Blade control structures because islands have no access to loop variables or conditional context. The workaround is to invert the nesting: put the loop inside the island and access the collection via $this->propertyName rather than a loop variable. This gives you one island for the whole list rather than one per item, which is also better for network efficiency.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.