Why Livewire Payloads Balloon
Every Livewire network round-trip serialises your component's public state into JSON, ships it to the server, re-hydrates the component, runs the action, then returns a diff. The more public properties you expose — and the larger their values — the heavier that payload becomes. On a component with a paginated Eloquent collection stored as a public property, you can easily hit tens of kilobytes per interaction.
The fix is not to abandon Livewire; it is to be deliberate about what lives in public state versus what is derived on demand.
Computed Properties: Derive, Don't Store
The single biggest payload win is replacing stored query results with #[Computed] properties. Livewire v3 memoises them for the lifetime of a single request, so multiple template references cost one query.
use Livewire\Attributes\Computed;
class UserTable extends Component
{
public string $search = '';
public int $perPage = 25;
#[Computed]
public function users(): LengthAwarePaginator
{
return User::query()
->when($this->search, fn ($q) => $q->where('name', 'like', "%{$this->search}%"))
->paginate($this->perPage);
}
public function render(): View
{
return view('livewire.user-table');
}
}
The $users paginator never touches the wire payload. Only $search (a short string) and $perPage (an integer) are serialised. The query runs fresh on each hydration — which is exactly what you want for paginated, filterable data.
Persisting Computed Values Across Requests
For expensive aggregates that do not change on every interaction, add persist: true:
#[Computed(persist: true)]
public function roleSummary(): array
{
return Role::withCount('users')->pluck('users_count', 'name')->all();
}
Livewire caches the return value in the session between requests. Use this sparingly — stale data is a real risk — and pair it with an explicit unset($this->roleSummary) call inside any action that mutates roles.
Lazy Hydration for Below-the-Fold Components
Nesting many Livewire components on a single page multiplies initial hydration cost. The #[Lazy] attribute defers a component's first render until the browser requests it:
use Livewire\Attributes\Lazy;
#[Lazy]
class RecentActivityFeed extends Component
{
public function placeholder(): View
{
return view('livewire.placeholders.activity-skeleton');
}
public function render(): View
{
return view('livewire.recent-activity-feed');
}
}
The parent page renders instantly; the browser fires a subsequent request for each lazy component. This trades one heavy synchronous render for several lighter async ones, which is almost always the right trade-off for dashboards.
Targeted Dirty-Tracking with #[Locked] and wire:model.live
Avoid wire:model.live on every input. It fires a network request on every keystroke. Reserve it for fields where immediate server feedback is genuinely needed (e.g., async validation). Use wire:model (deferred, syncs on next action) everywhere else.
For properties that should never be mutated from the client — IDs, tenant context, permission flags — add #[Locked]:
#[Locked]
public int $tenantId;
Livewire will throw a CorruptComponentPayloadException if the client attempts to overwrite it, preventing a class of mass-assignment-style vulnerabilities.
Trimming the Payload: Avoid Public Eloquent Models
Storing an entire Eloquent model as a public property serialises every attribute, including ones your template never reads. Prefer storing only the identifier and re-fetching via a computed property:
// Bad — full model in wire payload
public User $user;
// Good — only the ID travels over the wire
public int $userId;
#[Computed]
public function user(): User
{
return User::findOrFail($this->userId);
}
For deeply nested forms, consider a DTO backed by Livewire's #[Validate] attribute rather than a raw array, so only the fields you declare are serialised.
Takeaways
- Replace public query results with
#[Computed]properties — they are memoised per request and never serialised. - Use
persist: trueon expensive, rarely-changing aggregates; invalidate explicitly. - Apply
#[Lazy]to below-the-fold components to defer hydration cost. - Prefer
wire:model(deferred) overwire:model.liveunless real-time server feedback is required. - Mark immutable server-side properties with
#[Locked]to prevent client tampering. - Store only scalar identifiers in public state; re-hydrate full models via computed properties.