Livewire v3 Performance and State Management Patterns
Livewire v3 ships with a fundamentally different hydration model compared to v2. Every public property is serialized into a snapshot, sent to the browser, and re-hydrated on the next request. Understanding what lives in that snapshot — and what should not — is the single biggest lever for performance.
Keep Snapshots Lean
The snapshot payload is base64-encoded JSON attached to the DOM. Large Eloquent models, eager-loaded collections, or deeply nested arrays bloat it immediately.
// Bad: entire model in state
public User $user;
// Good: only the scalar you need
public int $userId;
public function getUser(): User
{
return User::find($this->userId);
}
For read-only derived data, reach for computed properties instead of storing results in public properties.
Computed Properties as a Cache Layer
Computed properties in v3 are memoized per request via #[Computed]. They are not serialized into the snapshot, so they cost nothing on the wire.
use Livewire\Attributes\Computed;
class OrderTable extends Component
{
public string $search = '';
public string $status = 'pending';
#[Computed]
public function orders()
{
return Order::query()
->when($this->search, fn ($q) => $q->where('reference', 'like', "%{$this->search}%"))
->where('status', $this->status)
->paginate(25);
}
public function render()
{
return view('livewire.order-table', [
'orders' => $this->orders,
]);
}
}
The query runs once per render cycle regardless of how many times the template references $this->orders. No caching boilerplate required.
Deferred and Live Wire:Model
Every wire:model without a modifier triggers a network request on every input event. For search boxes and filters that is almost always wrong.
<!-- Fires on every keystroke — avoid for expensive queries -->
<input wire:model="search">
<!-- Fires only on blur or Enter -->
<input wire:model.blur="search">
<!-- Debounced: fires 400 ms after the user stops typing -->
<input wire:model.live.debounce.400ms="search">
Use .live.debounce for search-as-you-type UX. Use .blur for form fields where instant feedback is not required. Reserve bare wire:model for checkboxes and selects where a single click is the natural commit point.
Nesting Components: When It Helps and When It Hurts
Nested components each carry their own snapshot. A parent re-render does not automatically re-render children — that is the upside. The downside is that each child adds its own hydration overhead and its own round-trip if it has reactive state.
// Stateless child: renders once, no round-trips of its own
class OrderRow extends Component
{
public Order $order; // passed as mount() argument
public function render()
{
return view('livewire.order-row');
}
}
For purely presentational rows, consider a plain Blade @include instead. Reserve child components for genuinely isolated interactive units (inline edit forms, toggleable detail panels).
Sharing State Between Components with entangle
When a parent needs to react to a child's state change without a full page re-render, entangle creates a two-way Alpine ↔ Livewire binding that stays in JavaScript until a server sync is needed.
<!-- Parent blade -->
<div x-data="{ open: $wire.entangle('drawerOpen') }">
<button @click="open = true">Open</button>
<livewire:order-drawer :wire:key="'drawer'" />
</div>
This keeps the toggle purely client-side while still allowing the Livewire component to read $this->drawerOpen on its next server request.
Practical Takeaways
- Snapshots are payload — store only scalar identifiers, never full models or collections.
#[Computed]is free on the wire; use it for any derived query result..blurand.debounceonwire:modeleliminate the majority of unnecessary round-trips.- Nested components are not free — use stateless Blade includes for rows and lists.
entanglebridges Alpine and Livewire state without extra server calls for purely UI-driven toggles.