Skip to content
Go back

I Asked AI to Audit My Page Speed. It Found 9 Issues.

Updated:

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

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

#IssueSeverityRoot cause
1AI chat mounting on every page loadCriticalHidden component still mounts
2Model $appends making HTTP requestsCriticalSerialization triggers API calls
3Query in render() instead of mount()HighRuns on every Livewire update
4Full model serialization in Like componentHightoArray() cascades through relationships
5Missing author eager-loadMediumN+1 query
6Comments loading in hidden modalMediumHidden component still mounts
7All goals loaded as public propertyLowPotential $appends trigger
8wire:poll.2s compounding render costLowPolls trigger full re-renders
9Team members loaded eagerlyLowComputed 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.


Back to top ↑