I’m not a developer. I’m a founder building a SaaS product with Laravel, Livewire, and Blade. Recently, I spent half a day working through performance issues on our campaigns page with an AI coding assistant. Along the way, I learned something that I think every non-technical founder building with Laravel needs to understand: the difference between Livewire components and Blade, and when to use each one.
This article is what I wish someone had explained to me before I started building. I’ll use real code examples from our codebase, real questions I asked, and real answers I got.
Table of contents
Open Table of contents
- What Are Blade and Livewire?
- The Comparison: When to Use Each
- The Real Story: How We Found This Out
- The Questions I Actually Asked (And the Answers)
- ”What are the downsides to making this change?”
- ”What about security implications?”
- ”If the ideas table is so much better, how come it still feels sluggish?”
- ”What about using Livewire 4 Islands?”
- ”How complex and risky is this change?”
- ”Overall, are we adding or removing code? Making things simpler or more complex?”
- ”It seems slower now after the change — tabs take forever to switch?”
- The Mental Model: Progressive Complexity
- The Practical Cost of Choosing Wrong
- A Simple Rule of Thumb
- What We Changed and the Results
- The Takeaway
What Are Blade and Livewire?
If you’re building with Laravel, you’ve encountered both of these. But understanding the difference matters more than most non-developers realise.
Blade
Blade is Laravel’s templating engine. It takes your data and renders it into HTML. Think of it like a sophisticated mail merge — you give it variables, it produces a web page. Once the page is sent to the browser, Blade’s job is done. It’s static output.
A Blade component is a reusable chunk of this template. Like a letterhead you use across different documents — it looks the same everywhere, but it doesn’t do anything on its own. It just displays things.
{{-- A Blade component: just displays data, no interactivity --}}
<x-user-avatar :user="$experiment->owner" />
Livewire
Livewire is a framework that sits on top of Blade. It adds interactivity. A Livewire component has a PHP class (the brain) paired with a Blade view (the face). The PHP class holds state — it remembers things between interactions. When a user clicks a button or types in a form, Livewire sends a request back to the server, runs your PHP code, and updates just the part of the page that changed.
Think of it like a live dashboard that responds to your clicks without reloading the whole page.
// A Livewire component: has state, responds to clicks, talks to the server
class SearchBar extends Component
{
public string $query = '';
public function updatedQuery()
{
// This runs every time the user types
// Results update live on the page
}
}
As Taylor Otwell, the creator of Laravel, puts it:
“IMO Livewire takes Blade to the next level. It’s basically what Blade should be by default.”
— Taylor Otwell, livewire.laravel.com
And critically:
“Some things people very desperately need to understand. Livewire is Blade. You can install it and only use it for a few components. Understanding these things will help you mature as a developer.”
— Taylor Otwell, X/Twitter, February 2025
The key insight: Livewire is not a replacement for Blade. It’s Blade with superpowers. You should only activate those superpowers when you actually need them.
The Comparison: When to Use Each
| Blade Component | Livewire Component | |
|---|---|---|
| What it does | Displays data. Static HTML output. | Manages state and responds to user interactions. |
| Has a PHP class? | Optional (can be anonymous) | Yes, always — with full lifecycle |
| Remembers state? | No. Renders once, done. | Yes. Tracks properties between requests. |
| Talks to the server? | No. Output is final. | Yes. Every interaction triggers a server round-trip. |
| Serialises to JSON? | No | Yes — entire component state embedded in page HTML |
| Good for | Displaying data, reusable UI fragments, table rows, cards, avatars, badges | Forms, search, filters, toggles, likes, modals, real-time updates |
| Performance cost | Almost zero | Mount, hydrate, render, snapshot per component |
| Scales in loops? | Yes, effortlessly | Multiplies overhead by number of iterations |
| Example | A table row showing a user’s name and avatar | A “like” button that toggles and saves to the database |
The official Livewire documentation makes this explicit:
“Before you extract a portion of your template into a nested Livewire component, ask yourself: Does this content need to be ‘live’? If not, we recommend that you create a simple Blade component instead.”
— Caleb Porzio, Livewire documentation
The Real Story: How We Found This Out
Our campaigns page at /campaigns/building felt slow. Not dramatically broken, just sluggish. I asked my AI coding assistant to investigate, and what it found was a textbook case of using Livewire where Blade would have been the right choice.
The Problem: 11 Livewire Components For One Table
Our campaigns table was rendering each row as its own Livewire component:
{{-- BEFORE: Each row is a full Livewire component --}}
@foreach ($this->experiments as $experiment)
<livewire:experiments.row
:experiment="$experiment"
:type="$type"
:key="md5('experiment-' . $experiment->id)"
/>
@endforeach
That single <livewire:experiments.row> tag looks harmless. But with 10 campaigns on the page, it meant 11 Livewire components running simultaneously — 1 parent table + 10 child rows.
Each of those 10 row components went through a full lifecycle:
- Mount — initialise the component with data
- Render — generate the HTML
- Snapshot — serialise the entire component state (including the full Eloquent model with every database field) into a JSON blob embedded in the HTML
- On updates — all 11 components must be dehydrated, sent to the server, rehydrated, re-rendered, and re-serialised
All that work, just to display a name, an avatar, and a date.
The Fix: Plain Blade Markup
The solution was to replace the Livewire component with plain inline Blade — the exact same HTML, just without the Livewire wrapper:
{{-- AFTER: Plain Blade markup, no component overhead --}}
@foreach ($this->experiments as $experiment)
<flux:table.row wire:key="experiment-{{ $experiment->id }}">
<flux:table.cell>
<div class="flex gap-4 items-center">
<flux:avatar size="sm"
src="{{ $experiment->experimentOwner?->profile_photo_url }}" />
<p>
{{ $experiment->experimentOwner?->id === auth()->id()
? 'You'
: $experiment->experimentOwner?->name }}
</p>
</div>
</flux:table.cell>
<flux:table.cell>
<a href="{{ route('experiments.edit', $experiment->id) }}"
wire:navigate>
{{ str()->limit($experiment->name, 53) }}
</a>
</flux:table.cell>
<flux:table.cell class="whitespace-nowrap">
<x-experiments.time-live :$experiment :$type />
</flux:table.cell>
<flux:table.cell>
<x-actions.action-menu :item="$experiment"
type="campaign" :showNewVersion="true" />
</flux:table.cell>
</flux:table.row>
@endforeach
The result: 7 files changed, 63 lines added, 267 lines removed. A net reduction of ~200 lines. One fewer PHP class. One fewer Blade view. Ten fewer Livewire components on every page load.
The page looks and behaves identically.
The Questions I Actually Asked (And the Answers)
Throughout this process, I asked a lot of questions. These are real — I think they’re the same questions any non-developer would ask.
”What are the downsides to making this change?”
This was about a different fix (caching a database query), but the pattern of questioning applies to every change. The answer was straightforward: there were no meaningful downsides. The cached version returned the same data, just without re-executing the query three times.
The lesson: always ask about downsides. Most performance fixes in this space are pure wins, but it’s worth checking.
”What about security implications?”
I asked this about caching, worried that we might be serving stale or wrong data. The answer: Livewire’s #[Computed] cache is per-request and per-user. It’s not a shared cache. Each user gets their own data on each page load. The cache just prevents the same query running multiple times within a single render.
”If the ideas table is so much better, how come it still feels sluggish?”
This was my “gotcha” moment. We’d identified that the ideas table used inline Blade (the right pattern), while the campaigns table used Livewire row components (the wrong pattern). But the ideas table didn’t feel fast either.
It turned out the ideas table had its own problems — unrelated to the Blade vs Livewire choice. A “like” button on each row was re-querying the database even though the parent had already fetched that data. The parent loaded the full stars relationship (all users who liked each idea) when it only needed a count. And there was a duplicate database filter running the same condition twice.
The lesson: Blade vs Livewire is an important choice, but it’s not the only thing that matters. You can make the right architectural choice and still have performance issues elsewhere.
”What about using Livewire 4 Islands?”
I’d heard about a new feature in Livewire 4 called “Islands” that promised lighter-weight components. Maybe that would solve the problem without refactoring?
After checking the documentation, the answer was clear: Islands cannot be used inside loops or @foreach blocks. They’re designed for isolated interactive widgets (like a chat panel or a notification bell), not for repeated elements like table rows.
This confirmed that plain Blade was the right choice for our table rows.
”How complex and risky is this change?”
I was nervous about refactoring a working page. The answer reassured me:
The row component has no independent state. It receives an
$experimentmodel from the parent and displays it. The only two interactive methods —copyExperimentandresetExperiment— are simple fire-and-redirect actions that don’t depend on any row-level state. They just need an experiment ID, which the parent already has.
In practice, this meant moving two methods from the child to the parent, and moving the HTML inline. No logic changed. No new behaviour was introduced.
”Overall, are we adding or removing code? Making things simpler or more complex?”
The answer:
We’re removing code and making things simpler. Roughly 70 lines removed, one fewer PHP class, one fewer Blade file, one fewer Livewire component to maintain. The codebase gets smaller.
The only minor trade-off: the parent Table component picks up two more methods. But it already had delete, assign, share, and comment actions — two more is consistent, not bloating.
”It seems slower now after the change — tabs take forever to switch?”
This was a scary moment. After making the change, I noticed tab switching getting progressively slower. We investigated and discovered something interesting: the slowness existed before our change too. We reverted the code and tested — same problem.
The real cause was a pre-existing wire:navigate memory leak pattern in Livewire’s SPA-mode navigation. Each tab click accumulated stale JavaScript in memory. A hard page refresh made it instantly fast again.
The lesson: test before AND after. Don’t assume a new change caused a pre-existing problem. We caught this by reverting and comparing.
The Mental Model: Progressive Complexity
Laravel’s entire philosophy is progressive complexity. You start with the simplest approach and only reach for more powerful tools when you actually need them:
- Need to display data? Use Blade.
- Need reusable display markup? Use a Blade component.
- Need interactive, stateful UI? Use a Livewire component.
Taylor Otwell describes this philosophy directly:
“Applications feel really interactive like they had tons of JavaScript behind them when really you’re just writing PHP and Blade the whole time. And you don’t have to deal with any complicated build steps or front-end scaffolding.”
— Taylor Otwell, Laracon US 2023
And:
“You basically are still writing your frontend in Blade, but you also pair it with a component on the backend.”
— Taylor Otwell, The Laravel Podcast
Caleb Porzio, the creator of Livewire, is equally direct about the cost of getting this wrong:
“It’s insanely fast to build stuff and fun to build stuff, but if you don’t know the ins and outs, you might step on a rake.”
— Caleb Porzio, Laracon US 2025
The Practical Cost of Choosing Wrong
It’s worth spelling out exactly what happens when you use a Livewire component where Blade would suffice, because it’s not obvious from the outside.
Each unnecessary Livewire component:
- Adds a PHP class instantiation and full lifecycle (mount, hydrate, render) on the server
- Serialises the entire component state into a JSON “snapshot” embedded in the HTML — including full Eloquent models with every database field
- Increases the HTML payload sent to the browser
- Adds a tracked component that Livewire’s JavaScript must morph and diff on updates
- Multiplies by the number of iterations — 10 rows means 10x all of the above
In our case, 10 row components meant 10 extra PHP class instantiations, 10 extra JSON snapshots (each containing a full campaign model), and 10 extra components for Livewire’s JavaScript to manage.
Removing them didn’t change what the user sees. It just removed unnecessary work.
A Simple Rule of Thumb
Before creating a Livewire component, ask yourself one question:
Does this piece of UI need to independently talk to the server?
- A “like” button that toggles when clicked? Yes — Livewire.
- A search bar that filters results as you type? Yes — Livewire.
- A table row that displays a name, a date, and an avatar? No — Blade.
- A card that shows a user’s profile picture? No — Blade.
- A form with validation and submission? Yes — Livewire.
- A dropdown menu that triggers a parent component’s action? No — Blade. (The parent Livewire component handles the action.)
Caleb Porzio captures this perfectly:
“Having a proper mental model when using Livewire is extremely helpful for debugging issues and even just understanding the constraints of the tool itself.”
— Caleb Porzio, calebporzio.com
What We Changed and the Results
Over the course of a single session, we made six targeted fixes:
| Fix | What Changed | Impact |
|---|---|---|
| Computed property cache bypass | $this->experiments() to $this->experiments | Eliminated 2 redundant database queries per page load |
| Lazy-load random quote | Moved ORDER BY RAND() query to #[Computed] | Query only runs when empty state is shown, not on every page |
| Like component N+1 | Passed boolean instead of re-querying per row | Eliminated 10 redundant queries per ideas page load |
| Remove unnecessary eager load | Removed stars from with() | Stopped loading full relationship data when only a count was needed |
| Remove duplicate filter | Removed redundant whereIn clause | Cleaner query, one less condition to maintain |
| Convert rows to Blade | Replaced Livewire row components with inline Blade | Eliminated 10 component lifecycles, removed ~200 lines of code |
Every one of these changes made the codebase smaller, simpler, and faster. None of them changed what the user sees.
The Takeaway
If you’re a non-developer building with Laravel and Livewire, the single most important thing to understand is this:
Livewire components are powerful. That power has a cost. Only pay it when you’re actually using the power.
Table rows, cards, list items, badges, avatars — these are display concerns. They render data. They don’t need their own state, their own lifecycle, or their own server communication channel. Use Blade.
Search bars, forms, toggles, filters, real-time updates — these are interactive concerns. They need to remember things and respond to user actions. Use Livewire.
Get this right, and your app will be faster, your codebase will be smaller, and your developers will thank you.
“It’s not really like this huge sprawling framework that takes over your whole application.”
— Taylor Otwell, The Laravel Podcast
That’s the point. Use it where it belongs. Use Blade everywhere else.