Skip to content

Laravel Inertia vs Livewire 4: How I Actually Choose

Laravel Inertia vs Livewire 4: How I Actually Choose

In February I started a client dashboard and spent a day and a half writing no code at all. I was arguing with myself about Livewire versus Inertia.

Livewire 4 had landed a few weeks earlier with islands and single file components. Inertia had shipped deferred props and prefetching. Both had quietly fixed the thing I used to reach for when ruling one of them out, which meant my old shortcut was gone and I had to think.

I picked Livewire. Six weeks later I moved two of the six screens to Inertia. Not because Livewire was wrong, but because I had been answering the wrong question the whole time.

The question is not which one is better. Both are good now, which is genuinely annoying. The question is where your state lives and who owns the render loop.

Livewire 4 closed the gap I used to complain about

My old objection was re-render granularity. In Livewire 3, if you wanted a region of the page to update without dragging the rest of the component along, you extracted it into a child component and paid for that with props, events, and a file you did not want.

Islands remove that tax. Caleb Porzio calls them the headline feature of the release in the Livewire 4 announcement, and I think that is fair:

<div>
    @island
        <div>
            Revenue: {{ $this->revenue }}
            <button wire:click="$refresh">Refresh</button>
        </div>
    @endisland

    <div>
        {{-- Untouched when the island refreshes --}}
        @include('dashboard.slow-table')
    </div>
</div>

The DOM part is nice. The database part is what sold me. Pair islands with computed properties and refreshing one island only runs that island’s queries. If your component reads three computed properties and only one of them is inside the island you refreshed, the other two never execute. That is isolation from Eloquent all the way out to the browser, and previously you got it only by splitting components.

Then there is the optimistic UI set, which killed most of the small Alpine snippets I had scattered around:

<button wire:click="save" class="data-loading:opacity-50">Save</button>

<div wire:show="$dirty">You have unsaved changes</div>

Likes: <span wire:text="likes"></span>

<input wire:model="message"
       wire:bind:class="message.length > 240 && 'text-red-500'">

None of those hit the server. wire:show toggles with CSS instead of removing the node. wire:text and wire:bind update immediately from client state. $dirty tracks unsaved changes without a round trip. In v3 each of those was three lines of Alpine that I wrote slightly differently every time.

Single file components are the change you notice first. PHP class, Blade template, scoped CSS, and component JavaScript in one file, and it is the default for php artisan make:livewire. New files get a lightning bolt emoji prefix so Livewire components stand out from Blade components in the tree. I turned the emoji off within about ten minutes, which the Livewire documentation tells you how to do, and kept everything else.

Inertia closed a different gap

My objection to Inertia was never the developer experience. It was that a controller returning six props waits on the slowest one, and one bad query holds the whole page hostage.

Deferred props fix exactly that:

// Before: 400ms permissions query blocks first paint
return Inertia::render('Users/Index', [
    'users' => User::all(),
    'roles' => Role::all(),
    'permissions' => Permission::all(),
]);

// After
return Inertia::render('Users/Index', [
    'users' => User::all(),
    'roles' => Role::all(),
    'permissions' => Inertia::defer(fn () => Permission::all()),
    'teams' => Inertia::defer(fn () => Team::all(), 'attributes'),
    'projects' => Inertia::defer(fn () => Project::all(), 'attributes'),
]);

The page renders with users and roles. Everything deferred arrives in follow-up requests. Props sharing a group name travel together, ungrouped ones go in parallel, and the group names are arbitrary strings you pick. On the client you wrap the dependent region:

import { Deferred } from "@inertiajs/react";

export default () => (
  <Deferred data="permissions" fallback={<div>Loading...</div>}>
    <PermissionsPanel />
  </Deferred>
);

The feature I did not expect to care about is rescue. Pass rescue: true to Inertia::defer() and an exception inside that callback gets reported through Laravel’s exception handler instead of failing the response, while the client renders a rescue slot with a retry button. A third party permissions API went down on me in week three and one panel showed a retry button instead of the page dying. The deferred props documentation covers the full shape of it.

Prefetching is the other half. Add prefetch to a Link and Inertia fetches the page data once the user has hovered for more than 75ms, caching it for 30 seconds by default. Both numbers are configurable. On a dashboard where people navigate the same four screens all day, this makes navigation feel like the data was already there, because it was.

The latency thing nobody wants to say out loud

Here is where I will push back on both marketing pages.

Livewire islands reduce payload size and query work. They do not remove the round trip. Any wire:click that calls a PHP method is still a network request, and no amount of island scoping changes the speed of light or the state of somebody’s mobile connection. The optimistic directives help precisely with the interactions that never needed the server, which is real but is not the same claim.

I have watched this land differently in two projects. An internal ops tool used from an office two hops from the server: nobody noticed anything, Livewire felt native. A field app used by inspectors on 4G in basements and car parks: everybody noticed, constantly, and no amount of wire:loading styling made a 900ms button feel acceptable.

Inertia moves that interaction into JavaScript, so the network only enters when you actually want data. You pay for it in a different currency. You now maintain a React application: a build pipeline, a dependency tree, hydration behaviour, and the state management question that never stops being a question. I have written up how I pick React state tools, and the useful thing about that post in this context is that on a Livewire project the entire decision simply does not exist.

Neither option is free. Pick the bill you would rather pay.

How I actually decide now

Four questions, in this order.

Does the screen need to stay responsive on a bad connection? If yes, Inertia. This is the only one that overrides everything below it.

Is the interactivity mostly CRUD, filters, modals, and tables sitting directly on Eloquent? If yes, Livewire, and it will not be close. Livewire 4 with islands does this with less code than any React setup I have built.

Who maintains this in eighteen months? If nobody on the team writes React by choice, Inertia’s ceiling is irrelevant because you will never reach it. I have watched a PHP team inherit a React front end they did not want, and the codebase aged about five years in one.

Do you need a specific npm component? A real datagrid with column pinning, a collaborative editor, a charting library with forty options. If yes, Inertia. Livewire can host these through Alpine, but you are writing glue and you will keep writing glue.

The answer nobody puts in comparison posts: you can run both. Route::livewire() for the admin screens, Inertia for the two that need to feel like an application. Same auth, same models, same deploy. That is what this dashboard became, and it has been fine for five months.

What I got wrong

I chose on feature lists. Both lists are long, both keep growing, and comparing them told me almost nothing. What actually decided the outcome was a two person PHP shop taking over maintenance. Two React screens is a rounding error for them. Six would have been a liability I created and then handed over.

I also assumed migration would hurt. Moving two screens took a day. The controllers barely changed because the queries already lived in the models where they belonged, and Livewire and Inertia both sit on the same Laravel underneath. If you have kept business logic out of your components, this decision is far cheaper to reverse than it feels while you are agonising over it. That has been true of most of the Laravel work I take on, and it is the argument for keeping components thin more than it is an argument for either library.

What I would do this week

On Livewire 3, pick your slowest dashboard screen, wrap the heaviest region in @island, and compare the network payload before and after. On Inertia, take the controller with the one slow query and move it into Inertia::defer().

Both take about twenty minutes and both replace an opinion with a number, which is a better basis for the argument you are about to have with yourself.