Home

9 minute read

Livewire 4: The Future of PHP Components

Tony Lea β€’

Livewire 4: The Future of PHP Components

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:

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:

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:

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

  1. Isolation: Actions outside the island don't trigger island re-renders
  2. Performance: Only the island content is processed for island-specific updates
  3. Responsiveness: The rest of your interface stays snappy

Before Islands:

After Islands:

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:

  1. Page loads instantly with placeholder
  2. Revenue section loads asynchronously
  3. 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:

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:


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:

Learning Curve

The beauty of islands is in their composability. Learn the basic primitive:

@island
    <!-- expensive content -->
@endisland

Then combine with modifiers:


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.