One of the most persistent myths in the TALL stack ecosystem is that "Livewire is slow" compared to React or Vue.
The truth is nuanced: Livewire is incredibly fast, but it punishes bad habits. If you treat a Livewire component exactly like a standard Blade controller, you will run into performance walls.
In this deep dive, we are going to take a sluggish, heavy Admin Dashboard and optimize it until it feels instant. We will cover Computed Properties, Lazy Loading, Isolating Components, and Network Payload Optimization.
🐌 Phase 1: The "Naive" Implementation
Imagine a dashboard showing User Stats, Recent Orders, and a Revenue Chart. A typical (slow) implementation looks like this:
1class AdminDashboard extends Component
2{
3 public $usersCount;
4 public $recentOrders;
5 public $revenueData;
6
7 public function mount()
8 {
9 // ❌ PROBLEM 1: Heavy queries running on EVERY component load
10 $this->usersCount = User::count();
11
12 // ❌ PROBLEM 2: Loading huge collections into public properties
13 // This dehydrates the ENTIRE collection to the frontend JSON payload
14 $this->recentOrders = Order::with('user')->latest()->take(10)->get();
15
16 // ❌ PROBLEM 3: Expensive aggregation logic
17 $this->revenueData = Order::aggregateRevenueByMonth();
18 }
19
20 public function render()
21 {
22 return view('livewire.admin-dashboard');
23 }
24}
Why is this slow?
- Hydration Tax: Every time you click a button (even one unrelated to orders), Livewire has to send those 10 orders back and forth between server and client.
- Blocking Render: The user sees a white screen until all those queries finish.
🧠 Phase 2: Computed Properties (The Caching Layer)
Livewire 3 introduced the #[Computed] attribute. This is game-changing. It caches the result of the method for the duration of the request and, crucially, allows you to not store data in public properties.
Refactoring:
1use Livewire\Attributes\Computed;
2
3class AdminDashboard extends Component
4{
5 // No more public properties for data!
6
7 #[Computed]
8 public function usersCount()
9 {
10 // Cache this for 60 seconds if it doesn't need to be real-time
11 return Cache::remember('stats.users', 60, fn() => User::count());
12 }
13
14 #[Computed]
15 public function recentOrders()
16 {
17 return Order::with('user')->latest()->take(10)->get();
18 }
19
20 // ...
21}
The Benefit: We drastically reduced the HTML payload size. Livewire no longer serializes the orders to the browser. It just renders the HTML and forgets the data.
💤 Phase 3: Lazy Loading (Perceived Performance)
Even with computed properties, if the database query takes 2 seconds, the user waits 2 seconds.
We can use Lazy Loading to render the page skeleton immediately and fetch the expensive data in the background.
Let's isolate the heavy chart into its own component: RevenueChart.php.
1namespace App\Livewire;
2
3use Livewire\Attributes\Lazy;
4use Livewire\Component;
5
6#[Lazy] // <--- The Magic Switch
7class RevenueChart extends Component
8{
9 public function mount()
10 {
11 // This expensive query now runs AFTER the page load
12 sleep(2); // Simulating slow DB
13 $this->revenueData = Order::aggregateRevenueByMonth();
14 }
15
16 public function placeholder()
17 {
18 // What the user sees while waiting
19 return view('livewire.placeholders.chart-skeleton');
20 }
21
22 public function render()
23 {
24 return view('livewire.revenue-chart');
25 }
26}
The Placeholder View:
Using Beartropy UI's primitives, we can build a beautiful skeleton state.
1<x-bt-card class="h-96 animate-pulse">
2 <div class="h-6 bg-gray-200 rounded w-1/4 mb-4"></div>
3 <div class="flex items-end space-x-2 h-64">
4 <div class="w-full bg-gray-200 rounded-t h-1/3"></div>
5 <div class="w-full bg-gray-200 rounded-t h-1/2"></div>
6 <div class="w-full bg-gray-200 rounded-t h-2/3"></div>
7 </div>
8</x-bt-card>
⚡ Phase 4: Network Optimization (Wire Navigate)
Now that our backend is optimized, let's look at the navigation experience. Standard links (<a href>) tear down the CSS/JS and reload the browser.
Livewire's wire:navigate turns your app into an SPA (Single Page Application) without the complexity.
In your Sidebar component:
1<x-bt-nav-link href="/dashboard" wire:navigate.hover>
2 Dashboard
3</x-bt-nav-link>
4
5<x-bt-nav-link href="/users" wire:navigate.hover>
6 Users
7</x-bt-nav-link>
The .hover Modifier:
This is a "cheat code". When the user hovers over the link, Livewire starts fetching the page in the background before they even click. By the time they release the mouse, the page is already there.
📉 Phase 5: Component Isolation
Imagine you have a "Date Filter" on your dashboard. In the naive implementation, changing the date re-renders the entire dashboard (Stats, Tables, Charts).
Instead, use Reactive Props or Events to update only what changed.
1
2<div>
3 <x-bt-date-picker wire:model.live="selectedDate" />
4
5
6 <livewire:revenue-chart :date="$selectedDate" />
7
8
9 <livewire:user-stats />
10</div>
Inside RevenueChart:
1class RevenueChart extends Component
2{
3 #[Reactive] // <--- Livewire 3 Feature
4 public $date;
5
6 // ...
7}
🏁 The Result
By applying these techniques, we moved from:
- 🔴 Initial Load: 2.5s (Blocking)
- 🔴 Payload: 450kb (Serialized Models)
- 🔴 Interaction: Sluggish full re-renders
To:
- 🟢 Initial Load: 150ms (Skeleton UI)
- 🟢 Payload: 12kb (HTML Only)
- 🟢 Interaction: Instant SPA feel with background pre-fetching
Livewire isn't slow. It just gives you the power to be fast—if you know how to use it.