This week at Laracon Caleb Porzio announced Livewire 4, which comes with unified components, performance improvements, and many more awesome goodies π€€
Unifying Livewire
Caleb takes the stage to address one of the most pressing issues in the Livewire's ecosystem: fragmentation. With three different ways to create Livewire components (traditional, Volt functional, and Volt class-based), the community had become divided, and newcomers were left confused about the "right" way to build components.
"The whole community, it's forked," Porzio admitted. "There's three different ways to make a livewire component... This is not good for newcomers, it's not good for old-timers, and everybody's guessing what's the best way."
His solution? A fourth way that unifies everything.
The New Default: Single-File Components
In Livewire 4, running php artisan make:livewire counter
creates a single-file, class-based component by default. This isn't Volt anymoreβit's just Livewire, powered by a custom parser.
<?php
use Livewire\Component;
new class extends Component {
public $count = 1;
public function increment()
{
$this->count++;
}
public function decrement()
{
$this->count--;
}
}; ?>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
<button wire:click="decrement">-</button>
</div>
<script>
this.watch('count', (value) => {
console.log('Count changed to', value);
});
</script>
What's New in Single-File Components
Native JavaScript Integration: No more @script
directive needed. Just add a <script>
tag at the bottom of your file, and use this.
instead of $wire
:
<script>
this.watch('count', (value) => {
console.log('Count changed to', value);
});
</script>
Interceptors: Hook into any part of the request lifecycle:
this.intercept('increment', ({ proceed, cancel }) => {
if (confirm('Are you sure?')) {
proceed();
} else {
cancel();
}
});
Multi-File Components: The Best of Both Worlds
Not everyone loves single-file components, and Porzio gets it. When you run make:livewire
on an existing component, you get this option:
php artisan make:livewire counter --mfc
This creates a multi-file component (MFC) structure:
β‘ counter/
βββ counter.php
βββ counter.blade.php
βββ counter.js
The PHP file is pure PHP (an anonymous class), the Blade file is pure Blade, and the JavaScript file is served as an ES6 module with automatic code-splitting and caching.
New Project Structure Opinions
Livewire 4 introduces opinionated folder structures to reduce decision fatigue:
resources/views/components/
- All your components (Blade and Livewire)resources/views/pages/
- Page-level componentsresources/views/layouts/
- Layout components
These directories are automatically namespaced, making component organization cleaner and more predictable.
File Naming: The Lightning Bolt Controversy
Here's where things get interesting (and controversial). Porzio introduced the lightning bolt emoji (β‘) as a file prefix for Livewire components:
β‘counter.livewire.php
While admitting he might be the only one who likes it, Porzio argued that Unicode emojis work across all file systems and provide instant visual recognition. The precedent exists in other frameworks (SvelteKit, Next.js) and even programming languages like Mojo.
"When did we stop having fun?" Porzio asked. "We're all gonna be out of jobs anyway... Let's have fun on our way out with emojis."
Leveraging PHP 8.4's Property Hooks
One of the most exciting features in Livewire 4 is deep integration with PHP 8.4's new property hooks:
Validation with Setters
public int $count {
set => max(1, $value);
}
This automatically prevents the count from going below 1, eliminating the need for updating hooks in many cases.
Computed Properties with Getters
public int $multiple {
get => $this->count * 5;
}
Access it in your template like any other property:
<div>Count: {{ $count }}, Times 5: {{ $multiple }}</div>
Advanced Caching with Property Hooks
You can even implement caching directly in property hooks:
public string $expensiveData {
get => cache()->remember("data-{$this->id}", 3600, fn() => $this->fetchExpensiveData());
set => cache()->put("data-{$this->id}", $value, 3600);
}
Asymmetric Visibility
PHP 8.4 also supports asymmetric property visibility:
public private(set) string $readOnlyProperty;
This replaces Livewire's #[Locked]
attribute with native PHP functionality.
Enhanced Loading States
Livewire 4 automatically adds data-loading
attributes to any element that triggers a network request:
<button wire:click="save" class="data-[loading]:opacity-50">
Save
</button>
<div class="data-[loading]:block hidden">
<span>Loading...</span>
</div>
This integrates beautifully with Tailwind CSS 4's data attribute selectors.
The Long-Awaited Slots Feature
After years of requests, Livewire 4 finally supports slots:
<livewire:modal>
<form wire:submit="save">
<input wire:model="title" />
<button type="submit">Save</button>
</form>
</livewire:modal>
The modal component works exactly like Blade components:
<!-- modal.blade.php -->
<div class="modal">
{{ $slot }}
</div>
Component Refs for Communication
Along with slots comes a new wire:ref
system for parent-child communication:
<livewire:modal wire:ref="modal">
<form wire:submit="save">
<!-- form content -->
</form>
</livewire:modal>
public function save()
{
// Save logic...
$this->dispatch('close')->to(ref: 'modal');
}
This eliminates the need for complex event broadcasting patterns when you need direct parent-child communication.
Ok, so far we covered the new rendering engine and pulling capabilities. Next, let's dive into the performance optimizations.
The Performance Problem: When Components Become Bottlenecks
Before diving into solutions, Caleb demonstrated a critical performance issue that many Laravel developers face but rarely measure. Using a simple benchmark, he showed how Blade components can become surprising bottlenecks:
// Simple benchmark: 25,000 Blade components in a loop
$start = microtime(true);
for ($i = 0; $i < 25000; $i++) {
// Render a simple Blade component
}
$end = microtime(true);
echo ($end - $start) * 1000 . " milliseconds";
The shocking results:
- First run (compilation): 508 milliseconds
- Second run (cached): 274 milliseconds
- Pure PHP require: 26 milliseconds
- Plain div + echo: 21 milliseconds
This revealed that even cached Blade components carry significant overhead - about 10x slower than plain PHP.
Blaze - The Blade Compiler Revolution
What is Blaze?
Blaze is a revolutionary Blade optimization layer that uses code folding to eliminate runtime overhead. Instead of aggressive caching (which creates cache management problems), Blaze analyzes your Blade templates at compile time and pre-renders static portions.
// Before Blaze - Complex compiled output with framework overhead
<?php $__env->startComponent('components.card'); ?>
<?php $__env->slot('title'); ?>Product Name<?php $__env->endSlot(); ?>
<div class="content"><?php echo e($product->description); ?></div>
<?php echo $__env->renderComponent(); ?>
// After Blaze - Clean, optimized output
<div class="card">
<h3 class="card-title">Product Name</h3>
<div class="content"><?php echo e($product->description); ?></div>
</div>
How Code Folding Works
Code folding identifies parts of your template that never change in production:
{{-- This Blade component --}}
<x-card class="bg-white shadow-lg">
<x-slot:title>{{ $title }}</x-slot:title>
<div class="p-4">
{{ $content }}
</div>
</x-card>
Blaze recognizes that the card structure, CSS classes, and HTML elements are static. Only the $title
and $content
variables are dynamic. It pre-renders the static parts at compile time.
Installation and Usage
composer require livewire/blaze
That's it! Blaze works transparently with your existing Blade templates. You can optionally opt-in with configuration, but the goal is zero-configuration optimization.
Performance Impact
Real-world results from the demo:
- Before Blaze: 29,000 views rendered in 1.6 seconds
- After Blaze: 100 views rendered in 131 milliseconds
That's more than a 10x performance improvement while maintaining the exact same developer experience.
Islands Architecture
The Problem with Monolithic Components
Traditional Livewire components re-render entirely on any update. Consider this dashboard:
class Dashboard extends Component
{
public function render()
{
return view('dashboard', [
'analytics' => $this->getAnalytics(), // Fast query
'revenue' => $this->getAccountRevenue(), // SLOW - 1 second query
'reports' => $this->getReports(), // Fast query
]);
}
public function generateReport()
{
// This action forces re-render of EVERYTHING
// Including the slow revenue calculation
}
}
Every action triggers the expensive getAccountRevenue()
query, making the entire interface sluggish.
Islands: Surgical Component Isolation
Islands allow you to isolate expensive parts of your template:
<div class="dashboard">
{{-- Fast section - always responsive --}}
<div class="analytics">
@foreach($analytics as $metric)
<x-metric :value="$metric->value" :label="$metric->name" />
@endforeach
</div>
{{-- Slow section - isolated on an island --}}
@island
<div class="revenue-chart">
<x-chart :data="$this->accountRevenue" />
</div>
@endisland
{{-- Fast section - always responsive --}}
<div class="reports">
<button wire:click="generateReport">Generate Report</button>
<button wire:click="downloadReport">Download</button>
</div>
</div>
Island Benefits
- Isolation: Actions outside the island don't trigger island re-renders
- Performance: Only the island content is processed for island-specific updates
- Responsiveness: The rest of your interface stays snappy
Before Islands:
- Generate Report: 1+ second (re-renders everything including slow revenue)
- Download: 1+ second (same problem)
After Islands:
- Generate Report: Instant (skips the island)
- Download: Instant (skips the island)
Lazy Loading with Islands
Islands support lazy loading with elegant placeholder handling:
@island(lazy: true)
@placeholder
<x-revenue-placeholder />
@endplaceholder
<div class="revenue-section">
<x-chart :data="$this->expensiveRevenueData" />
</div>
@endisland
User Experience:
- Page loads instantly with placeholder
- Revenue section loads asynchronously
- Smooth transition from skeleton to real data
Advanced Island Features
Named Islands with Remote Targeting
@island('reports')
@foreach($reports as $report)
<div class="report-item">{{ $report->title }}</div>
@endforeach
@endisland
{{-- This button is outside the island but targets it --}}
<button wire:island="reports" wire:click="loadMoreReports">
Load More
</button>
Flexible Rendering Modes
{{-- Default: Replace island content --}}
@island('reports')
<!-- content -->
@endisland
{{-- Append mode: Add new content to existing --}}
<button wire:island="reports" wire:click="loadMore" wire:render="append">
Load More
</button>
{{-- Prepend mode: Add content to the beginning --}}
<button wire:island="chat" wire:click="loadOlderMessages" wire:render="prepend">
Load Older Messages
</button>
Infinite Scroll with Intersection Observer
The ultimate modern UX pattern:
@island('posts', render: 'append')
@foreach($posts as $post)
<article class="post">{{ $post->content }}</article>
@endforeach
@endisland
{{-- Invisible trigger element --}}
<div wire:island="posts"
wire:intersect="$paginator.nextPage()"
class="h-4">
</div>
This creates true infinite scroll:
- β Only fetches new data when needed
- β Only renders new content
- β Preserves scroll position
- β Minimal DOM manipulation
- β Zero JavaScript required
Islands with Polling
Real-time updates for specific sections:
@island(poll: '5s')
<div class="live-metrics">
<span>Active Users: {{ $activeUsers }}</span>
<span>Revenue Today: ${{ number_format($todayRevenue) }}</span>
</div>
@endisland
Only the island polls - the rest of your page remains untouched.
Real-World Performance Results
The Complete Solution
Combining all three optimizations (new rendering engine + Blaze + Islands):
{{-- A complex dashboard that loads instantly and stays responsive --}}
<div class="dashboard">
{{-- Fast sections with Blaze optimization --}}
<x-metric-grid :metrics="$quickMetrics" />
{{-- Expensive section isolated and lazy-loaded --}}
@island('revenue', lazy: true)
@placeholder
<x-revenue-skeleton />
@endplaceholder
<x-revenue-chart :data="$expensiveRevenueData" />
@endisland
{{-- Interactive section with infinite scroll --}}
@island('reports', render: 'append')
@foreach($reports as $report)
<x-report-card :report="$report" />
@endforeach
@endisland
<div wire:island="reports"
wire:intersect="$paginator.nextPage()">
</div>
</div>
Performance characteristics:
- Initial load: Instant (lazy islands + Blaze optimization)
- User interactions: Always responsive (isolated islands)
- Real-time updates: Surgical precision (targeted polling)
- Infinite scroll: Smooth and performant (append rendering)
Developer Experience: Simple Yet Powerful
The Philosophy
Livewire v4's performance features follow a key principle: maximum performance with minimal complexity.
{{-- This is all you need for infinite scroll --}}
@island('posts', render: 'append')
@foreach($posts as $post)
<x-post :post="$post" />
@endforeach
@endisland
<div wire:island="posts" wire:intersect="$paginator.nextPage()"></div>
Compare this to traditional JavaScript approaches requiring:
- Complex state management
- Manual DOM manipulation
- Scroll position tracking
- Loading state handling
- Error boundary management
Learning Curve
The beauty of islands is in their composability. Learn the basic primitive:
@island
<!-- expensive content -->
@endisland
Then combine with modifiers:
lazy: true
- Lazy loadingpoll: '5s'
- Real-time updatesrender: 'append'
- Additive renderingwire:intersect
- Intersection triggers
Conclusion: A New Era for Laravel Frontend
Livewire v4 represents a massive leap forward for Laravel applications. For Laravel developers, this means you can build complex, interactive applications without sacrificing performance or developer experience. The era of choosing between "fast" and "feature-rich" is over.