Introduction
When our client approached us, their Laravel-based e-commerce platform was hemorrhaging sales. Pages took 4-6 seconds to load, the checkout process frequently timed out, and cart abandonment rates had skyrocketed to 78%. With Black Friday approaching, they needed a miracle—or at least a complete performance overhaul.
This is the story of how we transformed their struggling e-commerce platform into a high-performance shopping experience, reducing page load times from 4.5 seconds to 1.2 seconds—a 3.75x improvement—while increasing conversion rates by 43% and cutting infrastructure costs by 35%.
The Problem: A Slow-Motion Shopping Experience
Initial Performance Metrics
| Metric | Before Optimization |
|---|---|
| Homepage Load | 4.5 seconds |
| Product Page | 3.8 seconds |
| Category Pages | 5.2 seconds |
| Checkout Flow | 6.8 seconds |
| Database Queries | 127 per page |
| Cart Abandonment | 78% |
| Bounce Rate | 62% |
| Server Response (TTFB) | 2.1 seconds |
Root Causes Identified
- Database Nightmare: 127 queries per product page with multiple N+1 problems
- Zero Caching: No Redis, no query caching, no page caching
- Unoptimized Images: 5MB product images served directly
- Synchronous Processing: Email, inventory updates blocking requests
- Poor Asset Management: 2.3MB JavaScript bundle
- Inefficient Eloquent Usage: Loading entire models unnecessarily
Phase 1: Database Optimization (Week 1)
Problem: 127 Queries Per Product Page
The product detail page was a database massacre:
// ❌ Before: The performance killer
public function show($slug)
{
$product = Product::where('slug', $slug)->first(); // 1 query
$category = $product->category; // N+1 query #1
$brand = $product->brand; // N+1 query #2
$images = $product->images; // N+1 query #3
$relatedProducts = Product::where('category_id', $product->category_id)
->get(); // Query without eager loading
foreach ($relatedProducts as $related) {
$related->images; // N+1 query #4 (repeated)
$related->reviews; // N+1 query #5 (repeated)
}
$reviews = $product->reviews; // N+1 query #6
foreach ($reviews as $review) {
$review->user; // N+1 query #7 (repeated)
}
return view('products.show', compact('product', 'relatedProducts'));
}
Solution: Strategic Eager Loading
// ✅ After: Optimized with eager loading
public function show($slug)
{
$product = Product::with([
'category:id,name,slug',
'brand:id,name',
'images:id,product_id,url,alt_text',
'reviews' => fn($q) => $q->latest()->limit(10),
'reviews.user:id,name,avatar',
])
->withCount('reviews')
->withAvg('reviews', 'rating')
->where('slug', $slug)
->firstOrFail();
$relatedProducts = Product::with(['images' => fn($q) => $q->limit(1)])
->select('id', 'name', 'slug', 'price', 'category_id')
->where('category_id', $product->category_id)
->where('id', '!=', $product->id)
->inRandomOrder()
->limit(4)
->get();
return view('products.show', compact('product', 'relatedProducts'));
}
Result: Queries reduced from 127 to 6, database time from 1.8s to 120ms.
Phase 2: Strategic Indexing (Week 1)
Database Index Analysis
We analyzed slow query logs and added strategic indexes:
Schema::table('products', function (Blueprint $table) {
// Composite index for product listing
$table->index(['category_id', 'status', 'stock_quantity']);
// Covering index for search
$table->index(['name', 'slug', 'status']);
// Index for price filtering
$table->index(['price', 'status']);
});
Schema::table('orders', function (Blueprint $table) {
// Composite index for user order history
$table->index(['user_id', 'status', 'created_at']);
// Index for order lookup
$table->index('order_number');
});
Schema::table('order_items', function (Blueprint $table) {
// Foreign key indexes
$table->index(['order_id', 'product_id']);
});
// Full-text search index
Schema::table('products', function (Blueprint $table) {
DB::statement('ALTER TABLE products ADD FULLTEXT search_index(name, description)');
});
Query Optimization Examples
// ❌ Before: Unindexed query
Product::where('category_id', 5)
->where('status', 'active')
->where('stock_quantity', '>', 0)
->orderBy('created_at', 'desc')
->get();
// ✅ After: Leverages composite index
Product::where('category_id', 5)
->where('status', 'active')
->where('stock_quantity', '>', 0)
->latest()
->get();
// Execution time: 1.2s → 45ms
Result: Average query time reduced by 89%.
Phase 3: Multi-Layer Caching Strategy (Week 2)
1. Redis Cache Implementation
namespace App\Services;
class ProductCacheService
{
public function getProduct(string $slug)
{
return Cache::tags(['products'])
->remember("product:{$slug}", 3600, function () use ($slug) {
return Product::with([
'category:id,name,slug',
'brand:id,name',
'images:id,product_id,url',
])->where('slug', $slug)->first();
});
}
public function getCategoryProducts(int $categoryId, array $filters = [])
{
$cacheKey = "category:{$categoryId}:" . md5(json_encode($filters));
return Cache::tags(['products', "category:{$categoryId}"])
->remember($cacheKey, 1800, function () use ($categoryId, $filters) {
return $this->buildProductQuery($categoryId, $filters)->get();
});
}
public function clearProductCache(Product $product)
{
Cache::tags(['products'])->flush();
Cache::forget("product:{$product->slug}");
Cache::tags(["category:{$product->category_id}"])->flush();
}
}
2. Fragment Caching in Views
{{-- Cache expensive view fragments --}}
@cache('homepage-featured-products', 600)
<div class="featured-products">
@foreach($featuredProducts as $product)
@include('partials.product-card', ['product' => $product])
@endforeach
</div>
@endcache
@cache("product-reviews-{$product->id}", 1800)
<div class="reviews">
@foreach($product->reviews as $review)
@include('partials.review', ['review' => $review])
@endforeach
</div>
@endcache
3. API Response Caching
namespace App\Http\Middleware;
class CacheApiResponse
{
public function handle(Request $request, Closure $next, int $ttl = 300)
{
if ($request->method() !== 'GET') {
return $next($request);
}
$key = 'api:' . $request->path() . ':' . md5($request->getQueryString());
return Cache::remember($key, $ttl, function () use ($request, $next) {
return $next($request);
});
}
}
// Usage in routes
Route::middleware(['cache.response:600'])->group(function () {
Route::get('/api/products', [ProductApiController::class, 'index']);
Route::get('/api/categories', [CategoryApiController::class, 'index']);
});
Result: Cache hit ratio of 84%, response time reduced to 180ms.
Phase 4: Image Optimization (Week 2)
Problem: 5MB Product Images
Product images were killing performance:
- Average image size: 5MB
- No lazy loading
- No responsive images
- No WebP support
Solution: Comprehensive Image Strategy
namespace App\Services;
use Intervention\Image\Facades\Image;
class ImageOptimizationService
{
protected array $sizes = [
'thumbnail' => ['width' => 300, 'height' => 300],
'medium' => ['width' => 800, 'height' => 800],
'large' => ['width' => 1600, 'height' => 1600],
];
public function optimizeAndStore(UploadedFile $file, string $path): array
{
$urls = [];
foreach ($this->sizes as $size => $dimensions) {
// Resize and optimize
$image = Image::make($file)
->resize($dimensions['width'], $dimensions['height'], function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
})
->encode('webp', 85);
// Store to S3/CloudFront
$filename = "{$size}/" . Str::uuid() . '.webp';
Storage::disk('s3')->put($path . $filename, $image->stream());
$urls[$size] = Storage::disk('s3')->url($path . $filename);
}
return $urls;
}
}
Lazy Loading Implementation
{{-- Lazy load product images --}}
<img
src="{{ asset('images/placeholder.svg') }}"
data-src="{{ $product->image_medium }}"
data-srcset="{{ $product->image_medium }} 800w, {{ $product->image_large }} 1600w"
sizes="(max-width: 768px) 100vw, 50vw"
alt="{{ $product->name }}"
loading="lazy"
class="lazyload">
{{-- JavaScript for lazy loading --}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.3.2/lazysizes.min.js"></script>
Result: Image load time reduced from 3.2s to 0.4s, total page size from 12MB to 1.8MB.
Phase 5: Checkout Process Optimization (Week 3)
Problem: 6.8 Second Checkout
The checkout process was a conversion killer.
Solution: Streamlined Checkout Flow
namespace App\Services;
class OptimizedCheckoutService
{
public function processCheckout(array $data)
{
DB::transaction(function () use ($data) {
// 1. Create order (optimized query)
$order = Order::create([
'user_id' => auth()->id(),
'order_number' => $this->generateOrderNumber(),
'status' => 'pending',
'total' => $data['total'],
]);
// 2. Bulk insert order items (1 query instead of N)
$orderItems = collect($data['items'])->map(fn($item) => [
'order_id' => $order->id,
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'price' => $item['price'],
'created_at' => now(),
'updated_at' => now(),
]);
OrderItem::insert($orderItems->toArray());
// 3. Bulk update inventory (1 query instead of N)
$inventoryUpdates = $orderItems->mapWithKeys(fn($item) => [
$item['product_id'] => DB::raw('stock_quantity - ' . $item['quantity'])
]);
foreach ($inventoryUpdates as $productId => $decrement) {
Product::where('id', $productId)->update(['stock_quantity' => $decrement]);
}
// 4. Queue async operations (don't block checkout)
dispatch(new SendOrderConfirmation($order));
dispatch(new UpdateAnalytics($order));
dispatch(new NotifyWarehouse($order));
return $order;
});
}
}
Payment Integration Optimization
// ❌ Before: Blocking payment processing
public function checkout()
{
$result = $this->paymentGateway->charge($order);
Mail::to($user)->send(new OrderConfirmation($order));
$this->updateInventory($order);
return redirect()->route('order.success', $order);
}
// ✅ After: Non-blocking with queues
public function checkout()
{
$result = $this->paymentGateway->charge($order);
if ($result->success) {
dispatch(new ProcessOrderAsync($order));
return redirect()->route('order.success', $order);
}
}
Result: Checkout time reduced from 6.8s to 1.9s, cart abandonment dropped to 31%.
Phase 6: Frontend Optimization (Week 3)
1. Vite Asset Optimization
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'axios'],
checkout: ['stripe-js'],
},
},
},
cssCodeSplit: true,
chunkSizeWarningLimit: 1000,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
});
2. Code Splitting
// resources/js/app.js
import { createApp } from 'vue';
// Lazy load components
const ProductGallery = () => import('./components/ProductGallery.vue');
const ReviewSection = () => import('./components/ReviewSection.vue');
const RecommendedProducts = () => import('./components/RecommendedProducts.vue');
createApp({
components: {
ProductGallery,
ReviewSection,
RecommendedProducts,
}
}).mount('#app');
3. Critical CSS Extraction
@push('head')
<style>
{!! file_get_contents(public_path('css/critical.css')) !!}
</style>
@endpush
{{-- Load non-critical CSS async --}}
<link rel="preload" href="{{ asset('css/app.css') }}" as="style" onload="this.onload=null;this.rel='stylesheet'">
Result: JavaScript bundle reduced from 2.3MB to 450KB, First Contentful Paint improved to 0.8s.
Phase 7: CDN and Asset Delivery (Week 4)
CloudFront CDN Integration
// config/filesystems.php
'cloudfront' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_CLOUDFRONT_URL'),
],
// Helper function
function cdn_asset($path)
{
return config('filesystems.disks.cloudfront.url') . '/' . $path;
}
Browser Caching Headers
// Middleware for static assets
Route::middleware(['cache.headers:public;max_age=31536000;immutable'])->group(function () {
Route::get('/images/{path}', [AssetController::class, 'serve'])->where('path', '.*');
});
Result: Asset load time reduced by 72%, CDN cache hit ratio of 96%.
Phase 8: Search Optimization (Week 4)
Elasticsearch Integration
namespace App\Services;
use Elasticsearch\ClientBuilder;
class ProductSearchService
{
protected $client;
public function __construct()
{
$this->client = ClientBuilder::create()
->setHosts([env('ELASTICSEARCH_HOST')])
->build();
}
public function search(string $query, array $filters = [])
{
$params = [
'index' => 'products',
'body' => [
'query' => [
'bool' => [
'must' => [
'multi_match' => [
'query' => $query,
'fields' => ['name^3', 'description', 'brand'],
'fuzziness' => 'AUTO',
]
],
'filter' => $this->buildFilters($filters),
]
],
'size' => 24,
'from' => ($filters['page'] ?? 1 - 1) * 24,
]
];
$results = $this->client->search($params);
return $this->formatResults($results);
}
}
Result: Search response time reduced from 2.1s to 180ms.
Final Performance Metrics
Before vs After Comparison
| Metric | Before | After | Improvement |
|---|---|---|---|
| Homepage Load | 4.5s | 1.2s | 73% faster |
| Product Page | 3.8s | 1.0s | 74% faster |
| Category Page | 5.2s | 1.4s | 73% faster |
| Checkout | 6.8s | 1.9s | 72% faster |
| Database Queries | 127 | 6 | 95% reduction |
| Page Size | 12MB | 1.8MB | 85% reduction |
| TTFB | 2.1s | 0.3s | 86% faster |
| Cache Hit Rate | 0% | 84% | 84% increase |
Business Impact
| Metric | Before | After | Change |
|---|---|---|---|
| Conversion Rate | 1.2% | 1.72% | +43% |
| Cart Abandonment | 78% | 31% | -60% |
| Bounce Rate | 62% | 28% | -55% |
| Revenue per Visit | $2.40 | $4.10 | +71% |
| Server Costs | $4,200/mo | $2,730/mo | -35% |
Key Lessons Learned
1. Database Optimization First
Fixing N+1 queries and adding indexes provided the biggest immediate impact with minimal code changes.
2. Cache Everything Possible
84% cache hit ratio eliminated millions of database queries and computation cycles.
3. Async is King
Moving email, analytics, and inventory updates to queues made checkout feel instant.
4. Images Matter More Than Code
Optimizing images from 5MB to 200KB had more impact than any JavaScript optimization.
5. Measure, Don't Guess
New Relic and Laravel Telescope showed us exactly where to focus efforts.
Implementation Timeline
- Week 1: Database optimization & indexing
- Week 2: Caching implementation & image optimization
- Week 3: Checkout optimization & queue implementation
- Week 4: CDN, search, and frontend optimization
- Week 5: Testing, monitoring, and fine-tuning
Conclusion
Revamping the e-commerce platform for 3x faster performance wasn't about rewriting everything—it was about strategic optimizations at every layer. Laravel 12's powerful features—Eloquent optimization, Redis integration, queue system, and Vite—made these improvements achievable without architectural overhaul.
The results speak for themselves: 73% faster load times, 43% higher conversions, and 35% lower infrastructure costs. Most importantly, the platform was ready for Black Friday and handled 10x normal traffic without issues.
Key takeaways:
- Fix N+1 queries and add strategic indexes first
- Implement multi-layer caching strategy
- Optimize and lazy load images
- Move blocking operations to queues
- Use CDN for static asset delivery
- Implement full-text search with Elasticsearch
- Monitor everything and optimize continuously
Need help optimizing your e-commerce platform? NeedLaravelSite specializes in performance optimization and e-commerce development. Contact us for expert Laravel development services.
Related Resources: