Debug School

rakesh kumar
rakesh kumar

Posted on

Laravel Livewire Performance Checklist: Areas to Audit for Faster Loading and Fewer Requests

Use Alpine for modal visibility
Choose the correct model-binding timing
Limit NavbarComposer execution
Avoid unnecessary full-list reloads after saving
Keep public component state small

Livewire allows developers to build interactive pages using PHP and Blade. When an interaction requires server processing, Livewire sends a request, restores the component’s state, executes the action, and renders updated HTML.
Slow loading can happen when simple interactions trigger unnecessary requests, each request repeats expensive queries, or components exchange more data than they need.
The goal of Livewire hygiene is to reduce unnecessary work while preserving validation, permissions, and existing functionality.
The examples below are illustrative, not findings from HolidayLandmark’s actual code. They use Livewire 3 syntax. Check your version first:

composer show livewire/livewire
Enter fullscreen mode Exit fullscreen mode

Binding behavior differs between versions. For example, Livewire 3 uses wire:model.blur for a request on blur; Livewire 4 uses wire:model.blur.live. Laravel Livewire

  1. Use Alpine for modal visibility

What: Opening or closing a modal often changes only its visibility.

Why it becomes slow: A Livewire action used solely to toggle visibility adds a server round trip. The user waits for the response before seeing the modal.

When to optimize: When the modal’s content is already available and opening it does not require authorization or fresh data.

Where to inspect: Blade templates, modal components, and methods such as openModal() or closeModal().

Before: server-controlled visibility
// Livewire component

public bool $showModal = false;

public function openModal(): void
{
    $this->showModal = true;
}

<button wire:click="openModal">
    Open details
</button>

@if ($showModal)
    <div role="dialog" aria-modal="true">
        <p>Details already available on this page.</p>

        <button wire:click="$set('showModal', false)">
            Close
        </button>
    </div>
@endif
Enter fullscreen mode Exit fullscreen mode

Opening and closing both involve Livewire requests.
Solution: browser-controlled visibility

<div x-data="{ open: false }">
    <button type="button" @click="open = true">
        Open details
    </button>

    <div
        x-show="open"
        x-cloak
        @keydown.escape.window="open = false"
        role="dialog"
        aria-modal="true"
    >
        <p>Details already available on this page.</p>

        <button type="button" @click="open = false">
            Close
        </button>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Add this once to your stylesheet:

[x-cloak] {
    display: none !important;
}
Enter fullscreen mode Exit fullscreen mode

Why this works: Alpine updates visibility directly in the browser. Livewire supports Alpine for local interface interactions. Laravel Livewire
Preserve: Accessible focus management, validation messages, form reset behavior, and server-side authorization. An edit modal that loads a selected record still needs that loading step.
Check: Opening and closing a display-only modal should produce no Livewire request.

  1. Choose the correct model-binding timing

What: Model bindings control when input values synchronize with the component.

Why it becomes slow: Using .live on every field can trigger requests while the user types, even when the server only needs the values on submission.

When to optimize: Forms containing ordinary text fields, descriptions, or other inputs without immediate server dependencies.

Where to inspect: wire:model attributes and component methods such as updated() or updatedSearch().

Before: live updates for every field

<form wire:submit="save">
    <input wire:model.live="title">

    <textarea wire:model.live="description"></textarea>

    <button type="submit">Save</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Solution: synchronize ordinary fields on submission

<form wire:submit="save">
    <input wire:model="title">

    <textarea wire:model="description"></textarea>

    <button
        type="submit"
        wire:loading.attr="disabled"
        wire:target="save"
    >
        Save
    </button>
</form>

Enter fullscreen mode Exit fullscreen mode

In Livewire 3, plain wire:model synchronizes with the next action, such as submitting the form. Use .blur when a field needs server processing after losing focus, or debounced .live for interactive search. Laravel Livewire

{{-- Livewire 3: update when leaving the field --}}
<input wire:model.blur="email">

{{-- Search: update after a pause in typing --}}
<input wire:model.live.debounce.500ms="search">
Enter fullscreen mode Exit fullscreen mode

Why this works: The server processes input when it is useful rather than repeatedly during typing.

Preserve: Country-to-city dependencies, calculated prices, and required immediate feedback. Changing bindings does not automatically add validation; keep the relevant validation logic.

Check: Type into a normal form field and confirm no request occurs until submission.

  1. Limit NavbarComposer execution

What: A view composer prepares data whenever its registered views render.

Why it becomes slow: Registering an expensive composer for every view can repeat database queries or service calls across layouts, partials, and component rendering.

When to optimize: When a navbar composer is registered broadly but its data is needed only in the header.

Where to inspect: Service providers, composer registration, and the composer’s compose() method.

Before: registered for every view

use App\View\Composers\NavbarComposer;
use Illuminate\Support\Facades\View;

View::composer('*', NavbarComposer::class);

public function compose(\Illuminate\View\View $view): void
{
    $count = auth()->check()
        ? auth()->user()
            ->unreadNotifications()
            ->count()
        : 0;

    $view->with('unreadCount', $count);
}

Enter fullscreen mode Exit fullscreen mode

Solution: register only for the actual header view

View::composer(
    'partials.navbar',
    NavbarComposer::class
);

{{-- resources/views/partials/navbar.blade.php --}}
<nav>
    Notifications: {{ $unreadCount }}
</nav>
Enter fullscreen mode Exit fullscreen mode

Why this works: Views that do not need navbar data no longer execute its preparation logic.
Preserve: All header variants that require the data. If navbar information must change after an action, keep its refresh mechanism.
Check: Measure composer executions, database queries, and downstream calls before and after. Scoping does not guarantee only one execution if the header itself renders multiple times.

Prompt

Limit NavbarComposer to views that need its data, remove per-render cache-hit logs and the unused allNotifications key, and prevent it from overwriting unrelated view variables.
Run LocalizedTextComposer only on layouts.app after checking footer inheritance; verify all affected pages and preserve existing functionality and UX.

// Before: every view
View::composer('*', NavbarComposer::class);

// After: only views that need its data
View::composer(
    ['partials.header', 'home', 'organizers.show'],
    NavbarComposer::class
);
Enter fullscreen mode Exit fullscreen mode
  1. Avoid unnecessary full-list reloads after saving

What: A small edit should not always fetch every record again.

Why it becomes slow: Saving one row and reloading an entire list repeats database work and increases the data processed by the component.

When to optimize: When the saved field does not affect filtering, ordering, totals, or pagination membership.

Where to inspect: Save/update methods, refresh events, list-loading methods, and Blade row keys.

Before: reload every trip

public function updateName(int $id): void
{
    $data = $this->validate([
        'name' => ['required', 'string', 'max:255'],
    ]);

    $trip = Trip::findOrFail($id);
    $this->authorize('update', $trip);

    $trip->update($data);

    $this->trips = Trip::query()->get()->toArray();
}

Enter fullscreen mode Exit fullscreen mode

Solution: update the affected loaded row
This example assumes $trips contains a bounded list sorted by ID, with no name-based filter.

public function updateName(int $id): void
{
    $data = $this->validate([
        'name' => ['required', 'string', 'max:255'],
    ]);

    $trip = Trip::findOrFail($id);
    $this->authorize('update', $trip);

    $trip->update($data);
    $trip->refresh();

    foreach ($this->trips as $index => $row) {
        if ((int) $row['id'] === $id) {
            $this->trips[$index]['name'] = $trip->name;
            break;
        }
    }
}

@foreach ($trips as $trip)
    <div wire:key="trip-{{ $trip['id'] }}">
        {{ $trip['name'] }}
    </div>
@endforeach
Enter fullscreen mode Exit fullscreen mode

Why this works: It avoids a full-list database fetch. Livewire still renders the component; this is not a guarantee that only one row is processed or transmitted.

Preserve: Reload the current page when an edit changes sort position, filter membership, totals, or relationships.

Check: Confirm the displayed value matches the database and list behavior remains correct.

  1. Keep public component state small What: Public properties hold state that Livewire manages across requests.

Why it becomes slow: Large public arrays increase serialization, hydration, and network payload work.

When to optimize: Components storing all records, full API responses, nested relationships, or unnecessary columns.

Where to inspect: Public properties, mount(), data-loading methods, and response sizes.

Before: every record in public state

public array $trips = [];

public function mount(): void
{
    $this->trips = Trip::query()
        ->with(['organizer', 'bookings'])
        ->get()
        ->toArray();
}

Enter fullscreen mode Exit fullscreen mode

Solution: query only the required page and columns

use Livewire\Component;
use Livewire\WithPagination;

class TripList extends Component
{
    use WithPagination;

    public function render()
    {
        // Illustrative ownership scope:
        // replace with your actual access-control scope.
        $trips = Trip::query()
            ->where('organizer_id', auth()->id())
            ->select(['id', 'name', 'status'])
            ->orderByDesc('id')
            ->paginate(20);

        return view('livewire.trip-list', [
            'trips' => $trips,
        ]);
    }
}

<div>
    @foreach ($trips as $trip)
        <div wire:key="trip-{{ $trip->id }}">
            {{ $trip->name }} — {{ $trip->status }}
        </div>
    @endforeach

    {{ $trips->links() }}
</div>

Enter fullscreen mode Exit fullscreen mode

Why this works: The full dataset is no longer stored in a public array. The view receives a bounded page with only the columns it needs.
Livewire public properties are accessible through browser-side state, so they should not contain secrets or unnecessary sensitive data. Laravel Livewire
Preserve: Access-control scopes, search, pagination, relationships needed by the view, and editable form state.
Check: Compare request and response sizes, query time, and pagination behavior. Pagination still performs database work; reducing state does not eliminate queries.
Before applying these changes, record request counts, payload sizes, and response times for the same user actions. Apply one approved change at a time and repeat those measurements to establish whether it helped.

Priority Original point Kya pehle karna hai Expected impact
P1 #1 Server tuning OPcache sizing → config:cache + route:cache → log level Poore application par benefit, kyunki har request ka overhead kam hota hai.
P2 #2 Auth caching Profile-user lookup cache; non-retryable 4xx retries band Almost har authenticated click faster, repeated auth calls kam.
P3 #3 Remove HTTP N+1 Per-row API calls ko batch karo; unused calls hatao Lists aur dashboards par major improvement, especially zyada records hone par.
P4 #5 Queue + worker Email, notifications aur FCM background mein bhejo Invite, role-change aur payment saves faster; user ko background work ka wait nahi.
P5 #7 Livewire hygiene Unnecessary requests hatao; single row update; smaller payloads Daily UI interactions faster—typing, modal open/close aur saves.
P6 #4 Reference-data caching Countries, currencies, languages etc. cache with invalidation Forms aur repeated page loads faster, API/DB load kam.
P7 #8 Concurrency + transport Independent calls pool karo; FPM sizing aur timeouts tune karo Multiple services wale pages aur concurrent traffic par benefit.
P8 #6 Image pipeline Resize, stable previews, efficient uploads, asset caching Image-heavy pages aur uploads par major benefit; agar yahi main complaint hai toh P4 tak lao.
P9 #9 Database/growth hygiene Missing indexes, query fixes, limits; connection pooling where needed Large data/traffic par benefit; slow query confirmed ho toh uska fix turant higher priority par lao.

Top comments (0)