Why Livewire needs its own monitoring lens
Every user interaction — typing in a search box, clicking a checkbox, validating a field — is a POST to /livewire/update. A moderately interactive Livewire page can generate 10-50 backend requests per user-minute. That makes two things matter more than usual:
- Per-component aggregation — per-route grouping is useless because every request hits the same route. You care which component is slow.
- Latency targets — users expect UI to feel reactive. 300ms feels laggy; a normal HTTP request at 300ms feels fine.
Finding slow components
Your APM sees every Livewire interaction as a POST to /livewire/update. To find the slow ones, parse the request payload — it includes the component name:
Livewire 3 request shape (simplified)
{
"components": [
{
"snapshot": {
"memo": {
"name": "app.products.filter-panel",
"id": "abc123",
...
},
"data": { ... }
},
"updates": { ... },
"calls": [{ "method": "updateFilter", "params": [...] }]
}
]
}
Extract components[].snapshot.memo.name — that's the component class you want to group on.
Common Livewire performance bugs
N+1 inside render()
// Bad — render() fires on every interaction, each one refetches posts
public function render()
{
return view('component', [
'users' => User::all(), // 1 query
// Users->posts in template fires N queries per render
]);
}
// Good — eager-load
public function render()
{
return view('component', [
'users' => User::with('posts')->get(),
]);
}Unbounded real-time search
// Bad — fires on every keystroke. 100 chars = 100 requests.
public $search = '';
// Good — debounce, wait for pause
// In Blade:
// <input wire:model.live.debounce.300ms="search">
// Better — only update on blur for expensive filters
// <input wire:model.blur="search">Computed properties without caching
// Bad — recomputes on every render()
public function getCartTotalProperty()
{
return $this->cart->items->sum('price'); // hits DB each time
}
// Good — memoize
use Livewire\Attributes\Computed;
#[Computed(cache: true, seconds: 5)]
public function cartTotal()
{
return $this->cart->items->sum('price');
}Latency targets
| Interaction | p95 target | Feels laggy above |
|---|---|---|
| Toggle / checkbox | < 100ms | 200ms |
| Form validation | < 150ms | 300ms |
| Filter / search | < 300ms | 500ms |
| Submit / save | < 500ms | 1000ms |
For interactions you can't get below 200ms, use wire:loading indicators — a visible spinner or disabled state masks the latency.
Instrumenting lifecycle events
app/Providers/AppServiceProvider.php
use Livewire\Livewire;
public function boot(): void
{
Livewire::hook('component.mount', function ($component, $params) {
logger()->info('Livewire mount', [
'component' => $component::class,
'params' => array_keys($params),
]);
});
Livewire::hook('component.boot', function ($component) {
// Attach timing info, user context, etc.
});
}THE EASY WAY
NightOwl records every /livewire/update request
Filter to POST /livewire/update, sort by duration, find your slow interactions. Per-request trace drilldown shows which DB query or computed property ate the budget. Component-aware grouping is on the roadmap.
composer require nightowl/agent
php artisan nightowl:install