Livewire v3 Lazy Loading: Defer Component Rendering Without Breaking Your Layout
Livewire v3's #[Lazy] attribute defers component mounting to a follow-up browser request. Learn the mechanism, placeholder sizing, and why lazy loading is not a substitute for pagination.
07 Sept 2026, 10:09 UTC

If a Livewire component runs expensive queries in mount(), it slows down the entire initial page render — even if the user never scrolls to it. Livewire v3's lazy loading fixes this by skipping the component on the first render, outputting a lightweight placeholder, and mounting the real component in a follow-up request from the browser. The useful takeaway: lazy loading defers when work happens, not whether it happens, so it pays off only when the deferred work is genuinely heavy or below the fold.
How the mechanism works
On the initial page render, Livewire sees the lazy flag and does not execute the component's mount() or render(). Instead it emits the placeholder HTML plus a marker. Once the page loads in the browser, Livewire's JavaScript issues a second request per lazy component, the server mounts the component normally, and the returned HTML swaps in for the placeholder.
Two consequences follow directly from that design:
- Each lazy component costs one extra HTTP request. Ten lazy components means ten follow-up requests.
- The component's mount logic still runs — just later. Lazy loading is not query optimization.
A worked configuration
There are two equivalent ways to mark a component lazy. At the class level with the attribute:
use Livewire\Attributes\Lazy;
use Livewire\Component;
#[Lazy]
class RevenueChart extends Component
{
public function mount()
{
// Runs on the deferred request, not the initial page load.
}
public function placeholder()
{
return view('livewire.placeholders.revenue-chart');
}
public function render()
{
return view('livewire.revenue-chart');
}
}
Or inline in Blade when embedding the component, which is useful when the same component is lazy in some contexts and eager in others:
<livewire:revenue-chart lazy />
The placeholder() method returns what the user sees while the lazy request is in flight. If you omit it, Livewire renders a default empty placeholder — which usually means a collapsing hole in the page.
Match placeholder dimensions to avoid layout shift
The most visible mistake is a placeholder with different dimensions than the final component. When the real HTML swaps in, everything below it jumps. Give the placeholder an explicit height that approximates the mounted component — a skeleton block works well:
<!-- livewire/placeholders/revenue-chart.blade.php -->
<div class="h-64 w-full animate-pulse rounded-lg bg-gray-200"
aria-busy="true" aria-label="Loading revenue chart">
</div>
Verify with Lighthouse or the browser's Layout Shift tracking in DevTools (Performance panel) that Cumulative Layout Shift stays near zero during the swap.
Limits and common mistakes
Lazy is not pagination
A table rendering 5,000 rows lazily still renders 5,000 rows — one request later. If the problem is data volume, paginate or use wire:navigate with cursor pagination. Reserve lazy loading for components that are expensive but legitimately sized, such as charts, dashboards, or below-the-fold panels.
Mount logic must be cheap and idempotent
Because the lazy request is a normal Livewire lifecycle, N+1 queries or heavy service calls in mount() still execute — they just block the placeholder swap instead of the page load. Eager-load relationships and cache expensive aggregates. Idempotency matters because retries or double-loads (e.g., a user navigating away and back with wire:navigate) can trigger mount more than once.
Too many lazy components hurt more than they help
Each lazy component is a separate round trip. On a dashboard with eight lazy widgets, the browser fires eight requests at once, competing for server workers. Often a better split is: eager-render the two above-the-fold widgets, lazy-load the rest, or group related data into fewer components.
Interaction with wire:navigate
Lazy loading works with wire:navigate, but the placeholder-to-content swap happens after navigation completes, so pages can feel "half loaded" if everything is lazy. Keep at least the primary content eager so navigations feel instant.
How to verify it actually works
- Confirm your installed version: run
composer show livewire/livewirein your Laravel project root. The#[Lazy]attribute andlazyBlade parameter described here are Livewire v3 APIs; check the v3 docs for your exact minor version before relying on attribute names. - Load the page with the browser's Network tab open, filtered to XHR/fetch. You should see the initial HTML contain the placeholder, then a Livewire request that returns the component HTML.
- Temporarily add
sleep(3)inmount()and confirm the placeholder stays visible for roughly three seconds before the swap — this proves the work moved off the initial render. - Check your query log (e.g., Laravel Debugbar or
DB::listen) on both requests to confirm no N+1 queries run during the lazy mount.
If any of these checks fail — no follow-up request, placeholder never replaced, or queries still firing on initial load — re-check that the attribute is imported from Livewire\Attributes\Lazy and that the component is actually rendered via <livewire:...> syntax rather than a plain Blade include.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.