Livewire v3 Performance and State Management Patterns
#livewire #laravel #performance #frontend

Livewire v3 Performance and State Management Patterns

3 min read Mohamed Said Mohamed Said

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.
  • .blur and .debounce on wire:model eliminate the majority of unnecessary round-trips.
  • Nested components are not free — use stateless Blade includes for rows and lists.
  • entangle bridges Alpine and Livewire state without extra server calls for purely UI-driven toggles.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Does using #[Computed] in Livewire v3 cache the result across multiple requests?
No. #[Computed] memoizes the result within a single request lifecycle only. Each new HTTP round-trip re-executes the method. For cross-request caching, pass a cache TTL: #[Computed(cache: true)] stores the result in Laravel's cache keyed to the component instance.
Q02 When should I split a Livewire component into child components versus keeping everything in one component?
Split when a section has genuinely independent interactivity that should not trigger a full parent re-render — for example, an inline edit form inside a table row. Keep things in one component when the child would be purely presentational; a Blade @include is cheaper than a Livewire child component in that case.
Q03 Is wire:model.defer still available in Livewire v3?
The .defer modifier was removed in v3. Its replacement is wire:model (without .live), which now defers syncing to the next explicit Livewire request by default. Add .live to opt into real-time syncing, and combine it with .debounce for search inputs.

Continue reading

More Articles

View all