My Dashboard Scored 42 on PageSpeed Insights Until I Found Livewire’s Lazy Loading

I built an analytics dashboard for a small e-commerce client, sales charts, recent orders, top products, customer stats, all on one page. Looked great. Client was happy during the demo on my laptop.
Then I ran it through Google PageSpeed Insights out of curiosity, mostly just to add a good score to my portfolio notes. 42. Out of 100. On mobile it was worse.
My first assumption was images, that’s usually the culprit, right? Optimized every image, compressed everything, still barely moved the needle. Turns out the real problem was that every single Livewire component on that dashboard was loading fully, queries and all, before the page ever rendered anything to the screen. Six components, six separate sets of database queries, all blocking the initial page load at once.
That’s when I actually learned about Livewire’s lazy loading feature properly, and honestly, it’s one of those features that feels almost too simple for how much difference it makes.
What Was Actually Happening Behind The Scenes
Without lazy loading, every Livewire component on a page runs its mount() method and executes its queries immediately, all before the browser shows anything. If you’ve got a dashboard with a sales chart component pulling six months of order data, a top products component doing some heavier aggregation queries, and a recent activity feed, all of that happens synchronously before the page paints.
The user just stares at a blank white screen the whole time, wondering if the site is broken.
Step 1: Understanding Lazy Loading in Livewire
Lazy loading flips this order around. Instead of waiting for every component’s data to load before showing the page, Livewire immediately renders a placeholder for the component, shows the rest of the page right away, then loads the actual component’s data afterward with a quick follow-up request.
It sounds like it should slow things down since you’re now making extra requests, but the actual perceived speed improvement is huge, because users see something meaningful on screen almost instantly instead of waiting for everything at once.
Step 2: Turning It On (It’s Genuinely This Simple)
<livewire:sales-chart lazy />
That’s it. Adding the lazy attribute when including the component is the entire basic implementation. Livewire handles showing a placeholder and loading the real content automatically.
For my client’s dashboard, I went from this:
<livewire:sales-chart />
<livewire:top-products />
<livewire:recent-orders />
<livewire:customer-stats />
To this:
<livewire:sales-chart lazy />
<livewire:top-products lazy />
<livewire:recent-orders lazy />
<livewire:customer-stats lazy />
Four extra words, and the PageSpeed score jumped from 42 to 78 on the very next test. I genuinely didn’t expect that big of a jump from something so minor to implement.
Step 3: Adding a Proper Placeholder (Don’t Skip This)
By default, Livewire shows a very plain, empty placeholder while the component loads. On my first attempt, I left this default in place, and it actually looked worse than before, just an empty gap where the chart should be, with nothing indicating anything was happening.
You can customize this with a placeholder() method inside your component:
class SalesChart extends Component
{
public function placeholder()
{
return <<<'HTML'
<div class="animate-pulse bg-gray-200 rounded h-64 flex items-center justify-center">
<span class="text-gray-400">Loading sales data...</span>
</div>
HTML;
}
public function render()
{
return view('livewire.sales-chart');
}
}
Or, if you prefer keeping it in a separate Blade file for cleaner code (which I ended up doing across the whole project):
public function placeholder()
{
return view('livewire.placeholders.chart-skeleton');
}
This one change made the loading experience feel intentional rather than broken. Users saw a nice pulsing skeleton shape roughly matching the chart’s dimensions instead of a jarring empty gap, and it filled in smoothly a moment later.
Step 4: Being Selective About What Actually Needs Lazy Loading
Here’s where I initially overcorrected. After seeing how much lazy loading helped, I got a bit lazy myself (no pun intended) and just slapped lazy on every single component on every page across the entire client project, including small, fast components that didn’t need it at all.
This backfired a little. A tiny component just showing a static welcome message with the logged-in user’s name suddenly had an unnecessary loading flicker, because it now needed a placeholder and a follow-up request for something that used to render instantly with basically no performance cost at all.
Lesson learned: lazy loading is genuinely most useful for components doing actual heavy lifting, database queries, API calls, complex calculations, not for lightweight components that render near-instantly anyway. I went back through and removed lazy from about half the components I’d added it to, keeping it only on the genuinely heavy ones.
Step 5: Combining Lazy Loading With Actual Query Optimization
Lazy loading fixed the perceived speed issue, users saw the page immediately, but it didn’t fix the underlying problem that some of my queries were genuinely slow. The top products component, for example, was still taking almost 3 seconds to load once its lazy request fired, which is way too long even with a nice loading skeleton in front of it.
Checked it with Laravel Debugbar (a tool I keep installed on basically every project during development, genuinely useful for catching stuff like this) and found the query was doing an expensive aggregation across the entire orders table without any date range limit.
// Before - scanning everything
$topProducts = Product::withCount('orderItems')
->orderByDesc('order_items_count')
->take(5)
->get();
// After - limited to a relevant window, plus an index on the date column
$topProducts = Product::withCount(['orderItems' => function ($query) {
$query->where('created_at', '>=', now()->subDays(30));
}])
->orderByDesc('order_items_count')
->take(5)
->get();
Combined with adding an index on the created_at column in the orders table, that query dropped from nearly 3 seconds to under 200 milliseconds. Lazy loading made the slow load invisible to the user, but fixing the actual query made the experience genuinely fast, not just perceived as fast.
Step 6: Lazy Loading With A Loading Delay (For Genuinely Instant Components)
Something I discovered later that’s actually really useful, if a component loads fast enough, you don’t necessarily want the placeholder flashing on screen for a split second before instantly being replaced, since that itself can look a bit janky.
Livewire lets you control this with the lazy modifier combined with checking load time, though honestly for most projects I just accept the brief flash since it’s a reasonable tradeoff for a much faster initial page load overall. For anything that consistently loads in under 100ms, I just don’t bother lazy loading it in the first place, going back to my earlier point about being selective.
Real Example: The Full Before and After
For context, here’s roughly what changed on that e-commerce dashboard project:
Before lazy loading: Initial page load included 6 separate database query sets running synchronously, page took around 4.2 seconds to show anything meaningful, PageSpeed score of 42.
After lazy loading (with skeleton placeholders): Page showed layout and placeholders within about 400 milliseconds, individual components filled in over the next 1-2 seconds as their data loaded, PageSpeed score of 78.
After also optimizing the slow queries: Components that used to take 2-3 seconds now loaded in under 500ms each, PageSpeed score settled around 89.
That last number, 89, made the client genuinely happy since it’s comfortably in Google’s “good” range, and it directly affects SEO ranking too, which the client cared about since a chunk of their traffic came from organic search.
Common Mistakes I’d Warn You About
Lazy loading everything without thinking it through. As I mentioned, small, fast components don’t benefit and can actually feel worse with an unnecessary loading flicker added.
Leaving the default plain placeholder. It works, technically, but it looks unfinished. A simple skeleton loader with animate-pulse from Tailwind takes maybe five minutes to add and makes a real visual difference.
Assuming lazy loading fixes slow queries. It doesn’t. It just delays when the slowness happens and hides it behind a nicer loading state. If your component still takes 3+ seconds to load even lazily, you’ve got an underlying performance problem worth actually fixing.
Not testing on throttled network speeds. Everything felt instant on my office WiFi during development. Testing with Chrome DevTools’ network throttling set to “Slow 3G” showed me how the loading skeletons actually behaved for users on weaker connections, and it changed how I approached a few of the placeholder designs.
Forgetting mobile users entirely. The client’s actual customer base skewed heavily mobile, checking dashboard stats from their phones between other tasks. I tested this on an average Android device on regular mobile data (not WiFi) to get a realistic sense of load times, since laptop and office WiFi testing genuinely doesn’t reflect what a lot of real users experience.
Where This Has Helped On Other Projects Since
Beyond that dashboard, I’ve used the same lazy loading approach for:
- A restaurant ordering site’s menu page, where a “recommended for you” component loaded lazily while the actual menu items (the important part) appeared instantly
- A news website’s comment section, since comments genuinely don’t need to block the article content from loading
- An admin panel’s reporting page with several heavy chart widgets, similar to the original dashboard project
Final Thoughts
That 42 PageSpeed score was honestly a bit of a wake-up call. I’d gotten comfortable just building Livewire components without thinking much about how they actually loaded on a real page with multiple components stacked together. Lazy loading isn’t some advanced, complicated technique either, it’s genuinely a single word added to your component tag, plus a bit of extra effort on making the placeholder look decent.
If you’re building anything with multiple Livewire components on one page right now, especially ones doing real database work, run it through PageSpeed Insights before assuming it’s fine just because it feels fast on your own development machine. You might be surprised, like I was, at how much of a difference something this simple actually makes.



