Laravel E-Shop Homepage Speed Optimization: 5 Steps
Chapters8
Measures initial load and database queries using a debugging tool to establish a baseline for optimization.
A practical Laravel optimization walkthrough showing how to cut 100+ queries and sub-second load times through eager loading, selective column queries, and caching.
Summary
Laravel Daily’s video by author creates a realistic e-commerce homepage scenario for gaming. The host identifies a heavy 100+ query baseline and walks through concrete fixes: enable N+1 query detection (excluding production), eagerly load relations with with(), replace full subtrees with counts (withCount) to reduce data transfer, and trim columns to only what’s needed. He demonstrates how aggregated data like ratings and sales can be precomputed and cached, then invalidated via model observers. The result is a drop from ~700ms to well under a second, and eventually a cache-driven workflow that minimizes database hits to a few queries per page load. He also discusses how caching can be extended to sections like best sellers and top rated, and mentions potential UX improvements with client-side rendering for even faster perceived speed. The video ends by pointing to Laravel Daily’s Eloquent Expert course for deeper tips.
Key Takeaways
- Enabling global N+1 query detection (except production) in AppServiceProvider helps catch lazy loading issues early.
- Using with() to eagerly load relations fixes N+1 queries in controllers and blades, reducing repeated DB hits.
- Switching category children loading from full records to a count (withCount) lowers data transfer and speeds up queries.
- Aggregating data (like ratings and sales) in the database and selecting only necessary columns avoids expensive full-row fetches.
- Caching is implemented with cache()->remember for multiple sections (featured, best sellers, top rated) with five-minute TTLs.
- Cache invalidation is centralized via a model observer that forgets the home page cache on creates, updates, or deletes.
- Constants for cache keys and shared data structures prevent typos and simplify cache management across the codebase.
Who Is This For?
Essential viewing for Laravel developers optimizing large, data-heavy homepages or dashboards, especially those using Eloquent ORM and caching strategies. Useful for teams migrating heavy pages toward faster initial loads without sacrificing data freshness.
Notable Quotes
"So, in the beginning, we have 100 plus database queries, almost a second of data to load."
—Introduction to the baseline performance problem on the home page.
"The fix is to have with method, of course, loading the relationships."
—First step to resolve N+1 queries by eager loading.
"We don't need all the record of subcategory. So, we have categories children count, but it can be changed to children count here and with count."
—Optimization from loading full child records to counting children only.
"Cache remember for home page categories, for home page platforms. Those are very quick queries, to be honest, to the database, but those are even more rarely changed at all."
—Rationale for caching static-ish homepage data.
"On every created new product, updated product, and deleted, we have forget home page product cache."
—Cache invalidation strategy via model observers.
Questions This Video Answers
- How do I fix N+1 queries in Laravel with eager loading using with() and withCount()?
- What are best practices for caching a Laravel storefront homepage with multiple data sections?
- How can I reduce data transfer in Laravel when loading products with long descriptions or relations?
- How do Laravel model observers help manage cache invalidation for dynamic data?
Full Transcript
Hello guys. This video will be optimization of performance of this fictional home page for gaming website where you can browse games by category. This is fake data, but basically a lot of data on the home page of e-commerce. So, a lot of products with their relationships and it may load for quite a while. In this video, I will go through various steps of optimizations with eloquent queries, data loading, caching, and everything in between. So, in the beginning, we have 100 plus database queries, almost a second of data to load, and this is based by the way the idea came from.
I was browsing Upwork and some of the jobs here on Upwork I see are about performance optimizations or general fixing vibe coding as well. This was one of the jobs with Next.js in this case, but I just took the idea, not the implementation. So, front end Next.js, back end Laravel. Also, I didn't use any Meilisearch or any other API integration, but one of the optimization requirements was slow loading, long initial load time, slow response handling, slow page loading, and I decided to implement similar project like gaming which is described here, similar to gaming marketplaces such as Enaba.
And let me show you the before and the after in this video, step by step. Let's dive in. So, our starting point is the home page of e-commerce project. It's about gaming in this case with things like platforms and stuff like that, but the photos are of course fake, not about gaming. And what do we do first to measure the things, the performance on the website is to install some tool to measure. My first personal choice is still the same good old Laravel Debugbar. You can see that at the bottom. This is the package here.
It works still until this day, still fastest choice, but you can also use Laravel Telescope, Clockwork, or whatever you think is best. And here immediately we see the amount of database queries. And the home page load is almost a second. So, let's start fixing those N+1 queries. First, we need to enable the detection of N+1 query, and this can be done globally in the app service provider. So, in the boot method, you add this function prevent lazy loading and only not in production. This is important. And then model on top. Now, what happens if we reload the home page?
We reload and we should get an exception. Exactly as planned. So, whenever Laravel detects N+1 query, it will throw an exception and then you can debug and fix it. Important to enable that for all environments except production because you don't want 500 errors to happen on production server for live users. You better have performance issues there, but not 500s that the page wouldn't even be working. So, yeah, what do we fix here? So, we have featured with heroes and images. And I guess the images are not eager loaded. And in the controller, I discovered that there's products without images and without more things that were not eager loaded.
So, the fix is to have with method, of course, loading the relationships. So, that's the easy part. Now, I refresh the page and I get the same exception, but in another use case, category children. So, N+1 query and in another place in controller, add it with method. Refresh and another N+1 query in the blade where we have PHP block. By the way, PHP blocks in blade is not my favorite thing and I deliberately generated this piece of code to showcase that because more and more I see PHP blocks in Livewire specifically, and that strict logic with MVC is not that strict anymore among Laravel developers that I've noticed.
So, you may find n + 1 query in the blade as well. But, of course, the fix is not in the blade because it's just showing the information. The fix is in the controller adding another with statement. So, we refresh and we don't have n + 1 queries anymore. The page is loaded successfully now without any exceptions with wow, 29 queries. Great result. But, look at the time. It's still 700 milliseconds. Better, but not ideal. So, we continue with optimization and the next one I will show is how much data are we loading from the database.
So, the thing that I added just recently, category with children, I fixed n + 1 query, but we don't need the full children records. If you see how those categories are used on the front page, it's just the amount of subcategories. We don't need all the record of subcategory. So, we have categories children count, but it can be changed to children count here and with count in the controller. And now the database query will change. Now we have select asterisk from categories. We refresh the page. That still works, as you can see. We don't even have that subquery for subcategories anymore.
We have one query, select categories with select count of children count instead of all children records. And also another optimization related to count and averages. I already did that behind the scenes. So, these ratings, as you can see, the star ratings like the average and the amount of ratings for products. This is how it looks. So, product order by descending sales count or order by rating average or something similar and you don't see with count here or with count here because often it makes sense to aggregate the data in the database directly. So, whenever some rating comes up or sale comes up, you increase or recalculate that data directly in the products database table.
So, you aggregate the data not every time that the people are loading the home page on every product, but the data is already in the database aggregated once. Yes, the database table then it's bigger in terms of amount of data, but usually such optimization does pay off. It's kind of like caching, but on a database row level. Similar thing how we load the products here best sellers, top rated and others, we don't specify the select of the columns. So, as the comment says, select asterisk everywhere. And the product table may contain long descriptions, long text somewhere, including those publisher, categories, those could contain also long columns.
So, we download too much data to the home page. Let's fix that one. And look at the changes I've made behind the scenes. So, yes, I did add select, but look at that. This is a variable because we have multiple queries for the same thing or similar thing. So, I have this array, kind of a constant in the beginning of that method, and then in some cases we need to add a short description for featured products, otherwise we need just those columns. And also same for relations. With product relations, this is the default, but if we need to add something on top, we may also add it if we need it, but by default in this case those are always enough.
So, we load only the columns that we do need in the blade. So, the queries used to be like select asterisk from products, from publishers, from products and publishers again, and we had 37 megs of RAM. This is all in the RAM memory. What if we refresh now? We have 35 megs of RAM, which is not a huge improvement because we have like only like 20 to 30 products, so it's not a big deal, but it's more like a hygiene better practice to have only the columns listed that we need. And now we're down to 620 milliseconds, which is still not ideal.
Let's continue. So now after we optimized a lot of queries and data loading, the question is do we even need those queries because a lot of that data is pretty static, so it doesn't change every second or even every minute. So best sellers could be cached for at least like 5 minutes or maybe even full day. Of course, the time for caching and even possibility for caching depends on the situation. Maybe the price changes every second, who knows? It depends on the project and the data, but let me show you my implementation and we will see what would be the loading of this website after caching.
And this is the updated controller. Caching is pretty sophisticated here because we have a lot of similar data points. So this is one query now. So we have product query select as it used to be, but now we have two extra things. First, after get, we map every product with internal private function, I will get to that in a minute, into array and then we have cache remember we cache that data with specific key for featured section. It won't be the same for best seller cache section, but the same will be or maybe cache time.
And those are both configurable. Cache TTL minutes is on top in the same controller and then cache keys are in the product eloquent model. Of course, this is only one way of doing that, but constants on top of product model. Those are constants just to avoid mixing the strings and making typos or silly mistakes. So that's why the constants and we should use only the constants instead of direct strings. And similar things for best sellers and top rated, again, get the data, map, and then cache. Now, let's see what's inside of that product card data.
This is basically getting the details, adding or not adding short description depending on the scenario. That's why this is a parameter, and then return the array, which then needs to be cached. So, this function is a private function in the controller for specific the scenario because, as I said, we have a lot of overlapping or similar data structures. Then also, there was another query for hot deals. So, another cache remember with another different cache key, but for the same 5 minutes. Also, cache remember for home page categories, for home page platforms. Those are very quick queries, to be honest, to the database, but those are even more rarely changed at all.
So, why bother querying them from the database in the first place. And finally, the last cache for another query for home page stats. And then in the blade, we have a few changes related to how we show the data. So, we have arrays here in this case now. So, everywhere we had that data for products, including product card component, which has product cover URL now, also has arrays now. So, hero is also now hero name and short description are also arrays. And the last part of that equation is when the cache should be cleared. It's cached every 5 minutes by default with that parameter, as I showed.
But also, we need to invalidate the cache if something important changes. And this is, of course, again, very individual, depends on the project. But general strategy is to have observer. So, in the same product model, on top probably, yep, we have observed by, which is new syntax of Laravel 13 or 12, I don't remember exactly. I think it was released still in Laravel 12 and then Laravel 13 implemented more PHP attributes. So, anyway, we have product observer and then on every created new product, updated product, and deleted, we have forget home page product cache. And this is another benefit of constants in the model.
We have for each of those constants and cache forget. And those constants are here. So, not only we have individual constants, we have array of going through them also as constant as well to be used in observer in such for each loop. So, then the data stays in the cache either for 5 minutes or until some product is changed from admin panel or some import from wherever with eloquent model. And now let's try to reload the home page. I will do that in a separate browser tab, so we will see those results still for comparison.
And I refresh now, and nothing really changes first time. In fact, we have more queries, 46, because we have delete from cache, select from cache, and insert into cache. In this case, I'm using database driver for caching. Now, if I refresh again, look at that. Select from cache and 248 milliseconds. We have 10 queries from the database, and all those queries, almost all of them, are related to cache. So, we don't load product database table at all. So, yeah, these are kind of easy ones that you can optimize on eloquent level and on Laravel level and with caching, which already gives better performance.
But, of course, you can dive much deeper into, for example, re-rendering full HTML in cache, also using separate packages like Spatie response cache. Also, of course, the load of this home page depends on the image loading speed. And of course, this home page is just Laravel, which loads all at once. And if you use something like React or Livewire, whatever, with dynamically loaded data for each of those blocks, that also may be optimization because the home page would load at once with static HTML almost, and then would load the data in like points of a second separately with placeholder, so that may be even a better UX.
Let me know in the comments below if I should implement that and maybe shoot a separate video on that. But for this video, this optimization I showed is kind of table stakes, and I think in majority of cases, this optimization will already give great results. And if you want to see more of such eloquent optimizations or syntax interesting options, I have a course on Laravel Daily Eloquent Expert Level updated to Laravel 13 pretty recently. So, kind of small lessons here and there, two, three, four minutes of video with quick tip on certain syntax of Eloquent.
So, you can pick which lessons to watch and expand your Eloquent knowledge for more effective work with your database in Laravel projects. I will link that course in the description below. That's it for this time, and see you guys in other videos.
More from Laravel Daily
Get daily recaps from
Laravel Daily
AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.









