My app felt slow loading individual idea pages. Not broken — just sluggish. The kind of thing where you notice the delay but wouldn’t know where to start looking.
So I asked one question:
“the app feels a little slow when loading an individual idea, is there anything obvious in the code that could be having a negative impact on performance?”
Claude Code scanned the page’s code — the Livewire component, the Blade template, the models, and their relationships — and came back with 9 issues, prioritised by severity. I didn’t need to know what to look for. I just needed to ask.
If your Laravel app feels slow, try the same prompt on your slowest page. Here’s what it found on mine.
Table of contents
Open Table of contents
- The prompt that finds performance issues
- Critical: hidden component making API calls on every page load
- Critical: model $appends triggering HTTP requests on serialization
- High: expensive query in render() instead of mount()
- High: serializing an entire model unnecessarily
- Medium: missing eager-loading (N+1 queries)
- Medium: comments loading in a hidden modal
- The full list
- Quick win: reduce pagination on Livewire-heavy pages
- One character that tripled my database queries
- with() vs withCount() — loading data you never use
- Key takeaway
The prompt that finds performance issues
Before getting into the fixes, this is the important part. You don’t need to be a performance expert. You don’t need to know about N+1 queries or Livewire dehydration cycles. You just need to ask:
“This page feels slow — is there anything obvious in the code that could be having a negative impact on performance?”
Paste a URL or point to the component file. The AI will read the code, trace the data flow, and identify bottlenecks you’d never think to look for. The 9 issues below were all found from that single question.
Critical: hidden component making API calls on every page load
The idea edit page had a slide-over panel for AI-powered hypothesis building. The panel was hidden by default — but the Livewire component behind it still mounted on every page load.
// This runs on EVERY page load, even when the panel is hidden
public function mount(): void
{
$conversation = Conversation::query()->create([...]);
$this->js('$nextTick(() => $wire.streamResponse())');
}
Every time someone opened an idea, the app was creating a database record and firing an API call to OpenAI — for a feature the user hadn’t opened. This was the single biggest performance hit.
The fix
Don’t mount the component until the user actually opens the panel. Alpine’s x-if directive only renders the component when the condition is true:
<!-- Before: component mounts immediately, even when hidden -->
<livewire:hypothesis-ai-chat />
<!-- After: component only mounts when panel is opened -->
<template x-if="open">
<livewire:hypothesis-ai-chat />
</template>
The lesson: Livewire components inside hidden modals, slide-overs, and panels still mount and run their logic. If a component makes API calls or database queries on mount, it’s doing that work on every page load whether the user sees it or not.
Critical: model $appends triggering HTTP requests on serialization
The Goal model had appended attributes that fetched external data:
protected $appends = [
'last_month_data', // Makes an HTTP request
'previous_28_days_data', // Makes an HTTP request
'statusColor',
];
Every time a Goal was serialized — toArray(), JSON encoding, or Livewire’s dehydration cycle — these attributes triggered up to 3 external HTTP requests. And Goals were being serialized multiple times per page load across different components.
The lesson: $appends on Eloquent models run every time the model is serialized. If an appended attribute makes an API call or runs an expensive query, that cost multiplies every time the model passes through Livewire’s render cycle.
High: expensive query in render() instead of mount()
The component’s render() method was querying all idea IDs to calculate previous/next navigation:
// This runs on EVERY Livewire re-render, not just the first load
public function render()
{
$allIdeaIds = (clone $baseQuery)->pluck('id')->toArray();
$currentPosition = array_search($this->idea->id, $allIdeaIds);
// ...
}
In Livewire, render() runs on every update — every form field change, every button click, every poll interval. Expensive queries here multiply fast.
The lesson: Put expensive queries in mount() (runs once) rather than render() (runs on every update). If the data doesn’t change between updates, it shouldn’t be recalculated.
High: serializing an entire model unnecessarily
The Like component received a full Idea model and immediately called toArray():
public function mount(Idea $idea)
{
$this->record = $idea->toArray();
}
This serialized the entire Idea — including its Goal relationship, which triggered the $appends HTTP requests described above. The Like component only needed the idea’s ID and whether the current user had liked it.
The lesson: Don’t pass entire models to components that only need a few fields. Pass just the data you need.
Medium: missing eager-loading (N+1 queries)
The template accessed $idea->author without eager-loading the relationship. Each idea loaded its author with a separate query — the classic N+1 problem.
The lesson: If your template accesses a relationship (like $idea->author->name), make sure it’s eager-loaded with ->with('author') in the query.
Medium: comments loading in a hidden modal
The comments component queried all comments on mount, even though comments were inside a modal that the user might never open.
The lesson: Same pattern as the AI chat — hidden modals still mount their Livewire components. Either lazy-load with x-if, use Livewire’s #[Lazy] attribute, or move the query to a method that only runs when the modal opens.
The full list
| # | Issue | Severity | Root cause |
|---|---|---|---|
| 1 | AI chat mounting on every page load | Critical | Hidden component still mounts |
| 2 | Model $appends making HTTP requests | Critical | Serialization triggers API calls |
| 3 | Query in render() instead of mount() | High | Runs on every Livewire update |
| 4 | Full model serialization in Like component | High | toArray() cascades through relationships |
| 5 | Missing author eager-load | Medium | N+1 query |
| 6 | Comments loading in hidden modal | Medium | Hidden component still mounts |
| 7 | All goals loaded as public property | Low | Potential $appends trigger |
| 8 | wire:poll.2s compounding render cost | Low | Polls trigger full re-renders |
| 9 | Team members loaded eagerly | Low | Computed property loads all users |
Quick win: reduce pagination on Livewire-heavy pages
After fixing the individual idea page, I audited the ideas listing page. It felt sluggish too, but for a completely different reason.
The ideas table rendered 50 rows per page, and each row included a nested <livewire:like> component. That meant 50 separate Livewire components mounting on every page load — 50 mount() calls, 50 database queries, 50 sets of hydration overhead. Death by 50 small cuts.
how about we change to pagination being 10 as a starter? that would help a lot?
The fix was one line:
// Before: 50 nested Livewire components per page load
->paginate(50)
// After: 80% fewer components, queries, and hydrations
->paginate(10)
The same change applied to campaigns, which had the same pattern.
The lesson: If your Livewire table rows contain nested components (likes, actions, status badges), pagination count directly multiplies the component overhead. Dropping from 50 to 10 is a 30-second change that makes the page noticeably faster.
For the JavaScript side of this same performance audit — removing dead dependencies and cutting the bundle by 28% — see Your Laravel App Is Shipping JavaScript Nobody Uses.
One character that tripled my database queries
Livewire’s #[Computed] attribute caches the result of a method for the entire render cycle — but only when you access it as a property. Adding parentheses bypasses the cache entirely.
My campaigns table was calling the same computed method three times with parentheses:
// Before: parentheses bypass Livewire's computed cache — 3 identical queries
<x-heading :count="$this->experiments()->count()">
@if (!$this->experiments()->count())
@foreach ($this->experiments() as $experiment)
Removing the parentheses activates the cache:
// After: no parentheses = cached — 1 query, result reused 3 times
<x-heading :count="$this->experiments->count()">
@if (!$this->experiments->count())
@foreach ($this->experiments as $experiment)
One character change, two fewer database queries per page render.
The lesson: Always access Livewire #[Computed] methods as $this->property (no parentheses). Calling $this->property() with parentheses calls the raw method and skips the cache, running your query again every time.
with() vs withCount() — loading data you never use
The ideas page was eager-loading the full stars relationship AND counting it:
// Before: loads every star record into memory, then also counts them
Idea::with('stars')->withCount('stars')
The template only used the count — $idea->stars_count — to display a number. The full collection of star records was loaded into memory and never accessed.
// After: just the count — the only thing the template actually uses
Idea::withCount('stars')
The lesson: with('stars') loads every related record into a collection. withCount('stars') adds a single integer via a SQL subselect. If your template only displays a number, you’re loading data you never use. Check whether you actually access the relationship or just its count.
Key takeaway
You don’t need to be a performance expert to find and fix slow pages. Ask your AI assistant one question — “this page feels slow, what’s obvious?” — and let it trace the code for you. The most common patterns: components that mount when they shouldn’t, models that trigger expensive operations on serialization, and queries that run on every update instead of once on load.