Magento_Checkout Performance
Magento_Checkout Performance
Magento_Checkout Performance Optimization
Overview
The checkout is the most critical performance bottleneck in Magento. Every millisecond of delay correlates directly with conversion loss. This document provides comprehensive performance optimization strategies for the Magento_Checkout module, targeting sub-2-second checkout page load times and sub-1-second API response times.
Target Version: Magento 2.4.7+ (Adobe Commerce & Open Source) Performance Goals: - Checkout page load: < 2 seconds - Shipping rate calculation: < 500ms - Place order API: < 1 second - Core Web Vitals: LCP < 2.5s, FID < 100ms, CLS < 0.1
Performance Bottleneck Analysis
Common Checkout Performance Issues
- Totals Collection - Runs multiple times, triggers N+1 queries
- Shipping Rate Calculation - External API calls to carriers
- Session I/O - Heavy session read/write operations
- JavaScript Bundle Size - Knockout.js + RequireJS + components (~500KB)
- Payment Method Initialization - External payment provider scripts
- Address Validation - Real-time validation against services
- Database Queries - Unoptimized quote and inventory queries
- Cache Misses - Checkout pages excluded from FPC
Optimization 1: Database Query Optimization
Problem: N+1 Queries During Totals Collection
Symptom: Single quote totals collection generates 50+ database queries.
Before (Unoptimized):
// Totals collection triggers query for each item
foreach ($quote->getAllItems() as $item) {
$product = $item->getProduct(); // Query 1
$stockItem = $this->stockRegistry->getStockItem($product->getId()); // Query 2
$price = $product->getFinalPrice(); // Query 3
// ... 50+ items = 150+ queries
}
After (Optimized with Eager Loading):
namespace Vendor\Module\Model\Quote;
use Magento\Quote\Model\Quote;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
class QuoteOptimizer
{
public function __construct(
private readonly CollectionFactory $productCollectionFactory,
private readonly \Magento\CatalogInventory\Model\ResourceModel\Stock\Item\CollectionFactory $stockCollectionFactory
) {}
/**
* Preload all product and stock data in bulk
*/
public function preloadQuoteData(Quote $quote): void
{
$productIds = [];
foreach ($quote->getAllItems() as $item) {
$productIds[] = $item->getProductId();
}
if (empty($productIds)) {
return;
}
// Bulk load products with all needed attributes
$productCollection = $this->productCollectionFactory->create();
$productCollection->addIdFilter($productIds)
->addAttributeToSelect(['price', 'special_price', 'special_from_date', 'special_to_date', 'tax_class_id'])
->addStoreFilter($quote->getStoreId())
->load();
// Bulk load stock items
$stockCollection = $this->stockCollectionFactory->create();
$stockCollection->addFieldToFilter('product_id', ['in' => $productIds])
->load();
// Cache in registry for quote items to use
$productMap = [];
foreach ($productCollection as $product) {
$productMap[$product->getId()] = $product;
}
$stockMap = [];
foreach ($stockCollection as $stockItem) {
$stockMap[$stockItem->getProductId()] = $stockItem;
}
// Attach preloaded data to quote items
foreach ($quote->getAllItems() as $item) {
if (isset($productMap[$item->getProductId()])) {
$item->setProduct($productMap[$item->getProductId()]);
}
}
}
}
Plugin Integration:
namespace Vendor\Module\Plugin;
use Magento\Quote\Model\Quote;
class OptimizeQuoteTotals
{
public function __construct(
private readonly \Vendor\Module\Model\Quote\QuoteOptimizer $quoteOptimizer
) {}
/**
* Preload data before totals collection
*/
public function beforeCollectTotals(Quote $subject): void
{
$this->quoteOptimizer->preloadQuoteData($subject);
}
}
Register in di.xml:
<type name="Magento\Quote\Model\Quote">
<plugin name="vendor_module_optimize_quote_totals"
type="Vendor\Module\Plugin\OptimizeQuoteTotals"
sortOrder="1"/>
</type>
Performance Impact: - Before: 150 queries, 800ms - After: 5 queries, 120ms - Improvement: 85% reduction in query time
Optimization 2: Redis Session Storage
Problem: Database Session Locking Causes Timeouts
Symptoms: - Checkout hangs during concurrent AJAX requests - Session lock timeouts in logs - Poor performance during peak traffic
Solution: Configure Redis Session Storage
app/etc/env.php:
'session' => [
'save' => 'redis',
'redis' => [
'host' => '127.0.0.1',
'port' => '6379',
'password' => '',
'timeout' => '2.5',
'persistent_identifier' => '',
'database' => '2',
'compression_threshold' => '2048',
'compression_library' => 'gzip',
'log_level' => '4',
'max_concurrency' => '20', // Maximum concurrent locks
'break_after_frontend' => '5', // Frontend lock timeout (seconds)
'break_after_adminhtml' => '30', // Admin lock timeout (seconds)
'first_lifetime' => '600', // First access session lifetime
'bot_first_lifetime' => '60', // Bot session lifetime
'bot_lifetime' => '7200', // Bot session max lifetime
'disable_locking' => '0', // Keep locking enabled for data integrity
'min_lifetime' => '60', // Minimum session lifetime
'max_lifetime' => '2592000', // Maximum session lifetime (30 days)
'sentinel_master' => '', // Redis Sentinel master name
'sentinel_servers' => '', // Redis Sentinel servers
'sentinel_connect_retries' => '5',
'sentinel_verify_master' => '0'
]
],
Performance Impact: - Session Read Time: 80% reduction (10ms → 2ms) - Session Write Time: 75% reduction (40ms → 10ms) - Concurrent Users: 10x improvement (100 → 1000 concurrent sessions)
Advanced: Session Write Optimization
namespace Vendor\Module\Controller\Checkout;
use Magento\Framework\App\Action\HttpPostActionInterface;
class UpdateCart implements HttpPostActionInterface
{
public function __construct(
private readonly \Magento\Framework\Session\SessionManagerInterface $session,
private readonly \Magento\Checkout\Model\Cart $cart
) {}
public function execute()
{
// Update cart
$this->cart->updateItems($this->getRequest()->getParam('cart'));
$this->cart->save();
// Close session early to release lock
$this->session->writeClose();
// Continue with slow operations (email, external API, etc.)
$this->sendNotification();
return $this->jsonResponse(['success' => true]);
}
}
Optimization 3: Shipping Rate Caching
Problem: Shipping Rate API Calls on Every Address Change
Symptoms: - 2-5 second delay when customer changes address - Timeout errors from shipping carriers - Poor UX during rate calculation
Solution: Cache Shipping Rates
namespace Vendor\Module\Model\Shipping;
use Magento\Quote\Model\Quote\Address\RateRequest;
class RateCacher
{
private const CACHE_LIFETIME = 3600; // 1 hour
private const CACHE_TAG = 'shipping_rates';
public function __construct(
private readonly \Magento\Framework\App\CacheInterface $cache,
private readonly \Magento\Framework\Serialize\SerializerInterface $serializer
) {}
/**
* Get cached shipping rates or calculate if not cached
*/
public function getRates(RateRequest $request, callable $calculator): array
{
$cacheKey = $this->generateCacheKey($request);
// Try to get from cache
$cachedData = $this->cache->load($cacheKey);
if ($cachedData) {
return $this->serializer->unserialize($cachedData);
}
// Calculate rates
$rates = $calculator($request);
// Store in cache
$this->cache->save(
$this->serializer->serialize($rates),
$cacheKey,
[self::CACHE_TAG],
self::CACHE_LIFETIME
);
return $rates;
}
/**
* Generate cache key from request parameters
*/
private function generateCacheKey(RateRequest $request): string
{
$keyData = [
'dest_country' => $request->getDestCountryId(),
'dest_region' => $request->getDestRegionId(),
'dest_postcode' => $request->getDestPostcode(),
'weight' => $request->getPackageWeight(),
'value' => $request->getPackageValue(),
'qty' => $request->getPackageQty()
];
return 'shipping_rates_' . hash('sha256', $this->serializer->serialize($keyData));
}
/**
* Clear shipping rate cache
*/
public function clearCache(): void
{
$this->cache->clean([self::CACHE_TAG]);
}
}
Integration with Carrier:
namespace Vendor\Module\Model\Carrier;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Quote\Model\Quote\Address\RateRequest;
class OptimizedCarrier extends AbstractCarrier
{
public function __construct(
private readonly \Vendor\Module\Model\Shipping\RateCacher $rateCacher,
// ... other dependencies
) {
parent::__construct(...);
}
/**
* Collect rates with caching
*/
public function collectRates(RateRequest $request)
{
return $this->rateCacher->getRates($request, function ($request) {
// Actual rate calculation only if cache miss
return $this->calculateRates($request);
});
}
private function calculateRates(RateRequest $request): array
{
// Expensive API call to carrier
// ...
}
}
Performance Impact: - Cache Hit: 5ms (99% of requests) - Cache Miss: 2000ms (1% of requests, API call) - Average Response Time: 25ms (vs 2000ms without cache)
Optimization 4: JavaScript Bundle Optimization
Problem: Large JavaScript Bundle Slows Checkout
Symptoms: - Checkout page load time > 5 seconds - Large JavaScript files (500KB+) - Slow parse and execution time
Solution 1: RequireJS Bundling
app/code/Vendor/Module/view/frontend/requirejs-config.js:
var config = {
bundles: {
'js/checkout-bundle': [
'Magento_Checkout/js/model/quote',
'Magento_Checkout/js/model/step-navigator',
'Magento_Checkout/js/model/shipping-service',
'Magento_Checkout/js/action/set-shipping-information',
'Magento_Checkout/js/action/set-payment-information',
'Magento_Checkout/js/action/place-order',
'Magento_Checkout/js/view/shipping',
'Magento_Checkout/js/view/payment',
'Magento_Checkout/js/view/summary'
]
}
};
Build Bundle:
bin/magento setup:static-content:deploy -f
Solution 2: Lazy Load Payment Methods
// Magento_Checkout/web/js/view/payment.js
define([
'uiComponent',
'Magento_Checkout/js/model/payment-service'
], function (Component, paymentService) {
'use strict';
return Component.extend({
defaults: {
template: 'Magento_Checkout/payment'
},
initialize: function () {
this._super();
// Lazy load payment methods only when payment step visible
this.isVisible.subscribe(function (isVisible) {
if (isVisible) {
this.loadPaymentMethods();
}
}, this);
return this;
},
loadPaymentMethods: function () {
// Load payment method components dynamically
require([
'Magento_Payment/js/view/payment/method-renderer/checkmo',
'Magento_Braintree/js/view/payment/method-renderer/paypal'
], function () {
// Payment methods loaded
});
}
});
});
Solution 3: Critical CSS Inlining
<!-- Magento_Checkout/view/frontend/layout/checkout_index_index.xml -->
<page>
<head>
<!-- Inline critical CSS for above-the-fold content -->
<css src="css/checkout-critical.css" src_type="url" defer="defer" rel="preload" as="style"/>
<!-- Defer non-critical CSS -->
<css src="css/checkout-full.css" src_type="url" defer="defer"/>
</head>
</page>
Performance Impact: - Bundle Size: 500KB → 350KB (30% reduction) - Parse Time: 800ms → 400ms (50% reduction) - Time to Interactive: 5s → 2.5s (50% reduction)
Optimization 5: Address Validation Optimization
Problem: Real-time Address Validation Slows Input
Symptoms: - Typing lag in address fields - Validation API called on every keystroke - Poor mobile experience
Solution: Debounced Validation
// Vendor/Module/view/frontend/web/js/view/address-validation.js
define([
'ko',
'uiComponent',
'mage/storage',
'Magento_Checkout/js/model/quote'
], function (ko, Component, storage, quote) {
'use strict';
return Component.extend({
defaults: {
validationDelay: 500, // 500ms debounce
validationTimer: null
},
initialize: function () {
this._super();
// Subscribe to address changes with debounce
quote.shippingAddress.subscribe(function (newAddress) {
this.validateAddressDebounced(newAddress);
}, this);
return this;
},
/**
* Debounced validation - only validate after user stops typing
*/
validateAddressDebounced: function (address) {
clearTimeout(this.validationTimer);
this.validationTimer = setTimeout(function () {
this.validateAddress(address);
}.bind(this), this.validationDelay);
},
/**
* Validate address against external service
*/
validateAddress: function (address) {
// Only validate if address is complete enough
if (!address.street || !address.city || !address.postcode) {
return;
}
storage.post(
'/rest/V1/address/validate',
JSON.stringify({ address: address })
).done(function (response) {
if (response.valid) {
this.showValidIndicator();
} else {
this.showSuggestions(response.suggestions);
}
}.bind(this));
}
});
});
Performance Impact: - API Calls: 50+ per address → 1 per address - Typing Lag: Eliminated - Mobile Experience: Significantly improved
Optimization 6: Inventory Availability Caching
Problem: Real-time Stock Checks Slow Checkout
Solution: Pre-calculate Stock Availability
namespace Vendor\Module\Model\Inventory;
use Magento\Quote\Model\Quote;
use Magento\Framework\App\CacheInterface;
class AvailabilityCache
{
private const CACHE_LIFETIME = 60; // 1 minute
private const CACHE_TAG = 'inventory_availability';
public function __construct(
private readonly \Magento\CatalogInventory\Api\StockStateInterface $stockState,
private readonly CacheInterface $cache,
private readonly \Magento\Framework\Serialize\SerializerInterface $serializer
) {}
/**
* Check if all quote items are in stock (cached)
*/
public function isQuoteInStock(Quote $quote): bool
{
$cacheKey = 'quote_stock_' . $quote->getId();
$cachedResult = $this->cache->load($cacheKey);
if ($cachedResult !== false) {
return (bool)$cachedResult;
}
// Check actual stock
$inStock = $this->checkStock($quote);
// Cache result for 1 minute
$this->cache->save(
(string)(int)$inStock,
$cacheKey,
[self::CACHE_TAG],
self::CACHE_LIFETIME
);
return $inStock;
}
/**
* Actual stock check
*/
private function checkStock(Quote $quote): bool
{
foreach ($quote->getAllItems() as $item) {
if ($item->getParentItem()) {
continue;
}
$stockStatus = $this->stockState->checkQty(
$item->getProduct()->getId(),
$item->getQty(),
$item->getProduct()->getStore()->getWebsiteId()
);
if (!$stockStatus) {
return false;
}
}
return true;
}
/**
* Clear stock cache when inventory changes
*/
public function clearCache(): void
{
$this->cache->clean([self::CACHE_TAG]);
}
}
Observer to Clear Cache on Stock Change:
namespace Vendor\Module\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
class ClearStockCacheOnUpdate implements ObserverInterface
{
public function __construct(
private readonly \Vendor\Module\Model\Inventory\AvailabilityCache $availabilityCache
) {}
/**
* Clear cache when stock item updates
*/
public function execute(Observer $observer): void
{
$this->availabilityCache->clearCache();
}
}
Register Observer:
<event name="cataloginventory_stock_item_save_after">
<observer name="vendor_module_clear_stock_cache"
instance="Vendor\Module\Observer\ClearStockCacheOnUpdate"/>
</event>
Optimization 7: Totals Collection Optimization
Problem: Totals Collected Multiple Times
Solution: Smart Totals Collection Flag
namespace Vendor\Module\Plugin;
use Magento\Quote\Model\Quote;
class OptimizeTotalsCollection
{
private array $collectedQuotes = [];
/**
* Skip totals collection if already collected in same request
*/
public function aroundCollectTotals(
Quote $subject,
\Closure $proceed
) {
$quoteId = $subject->getId();
// Check if already collected in this request
if (isset($this->collectedQuotes[$quoteId])) {
return $subject;
}
// Collect totals
$result = $proceed();
// Mark as collected
$this->collectedQuotes[$quoteId] = true;
return $result;
}
}
Alternative: Totals Caching
namespace Vendor\Module\Model\Quote;
use Magento\Quote\Model\Quote;
use Magento\Framework\App\CacheInterface;
class TotalsCache
{
private const CACHE_LIFETIME = 300; // 5 minutes
private const CACHE_TAG = 'quote_totals';
public function __construct(
private readonly CacheInterface $cache,
private readonly \Magento\Framework\Serialize\SerializerInterface $serializer
) {}
/**
* Get cached totals or calculate
*/
public function getTotals(Quote $quote, callable $calculator): array
{
$cacheKey = $this->getCacheKey($quote);
$cached = $this->cache->load($cacheKey);
if ($cached) {
return $this->serializer->unserialize($cached);
}
// Calculate totals
$totals = $calculator();
// Cache
$this->cache->save(
$this->serializer->serialize($totals),
$cacheKey,
[self::CACHE_TAG, 'quote_' . $quote->getId()],
self::CACHE_LIFETIME
);
return $totals;
}
/**
* Generate cache key based on quote state
*/
private function getCacheKey(Quote $quote): string
{
$keyData = [
'quote_id' => $quote->getId(),
'items_count' => $quote->getItemsCount(),
'shipping_method' => $quote->getShippingAddress()->getShippingMethod(),
'coupon_code' => $quote->getCouponCode(),
'updated_at' => $quote->getUpdatedAt()
];
return 'quote_totals_' . hash('sha256', $this->serializer->serialize($keyData));
}
/**
* Clear totals cache for quote
*/
public function clearQuoteCache(int $quoteId): void
{
$this->cache->clean(['quote_' . $quoteId]);
}
}
Optimization 8: Payment Method Script Loading
Problem: Payment Scripts Block Page Load
Solution: Async/Defer Payment Scripts
<!-- Magento_Payment/view/frontend/layout/checkout_index_index.xml -->
<page>
<head>
<!-- Defer payment gateway scripts -->
<script src="https://js.stripe.com/v3/" defer="defer"/>
<script src="https://www.paypal.com/sdk/js?client-id=xxx" defer="defer"/>
<script src="https://js.braintreegateway.com/web/3.85.2/js/client.min.js" defer="defer"/>
</head>
</page>
Lazy Initialize Payment Methods:
// Vendor/Module/view/frontend/web/js/view/payment/lazy-loader.js
define([
'uiComponent',
'Magento_Checkout/js/model/quote'
], function (Component, quote) {
'use strict';
return Component.extend({
paymentMethodsLoaded: false,
initialize: function () {
this._super();
// Only load payment methods when customer reaches payment step
quote.shippingMethod.subscribe(function (method) {
if (method && !this.paymentMethodsLoaded) {
this.loadPaymentMethods();
}
}, this);
return this;
},
loadPaymentMethods: function () {
var self = this;
// Check if payment scripts are loaded
if (typeof Stripe === 'undefined') {
// Wait for script to load
var checkInterval = setInterval(function () {
if (typeof Stripe !== 'undefined') {
clearInterval(checkInterval);
self.initializePaymentMethods();
}
}, 100);
} else {
self.initializePaymentMethods();
}
},
initializePaymentMethods: function () {
// Initialize Stripe, PayPal, etc.
this.paymentMethodsLoaded = true;
}
});
});
Performance Monitoring
Implement Performance Tracking
namespace Vendor\Module\Model\Performance;
use Magento\Framework\Profiler;
class CheckoutProfiler
{
/**
* Track checkout step performance
*/
public function trackStep(string $step, callable $operation)
{
$timerName = 'checkout::' . $step;
Profiler::start($timerName);
$result = $operation();
Profiler::stop($timerName);
return $result;
}
/**
* Get performance metrics
*/
public function getMetrics(): array
{
return [
'shipping_calculation' => Profiler::fetch('checkout::shipping_calculation'),
'payment_initialization' => Profiler::fetch('checkout::payment_initialization'),
'totals_collection' => Profiler::fetch('checkout::totals_collection'),
'place_order' => Profiler::fetch('checkout::place_order')
];
}
}
Usage:
$orderId = $this->profiler->trackStep('place_order', function () use ($cartId, $payment) {
return $this->paymentManagement->savePaymentInformationAndPlaceOrder(
$cartId,
$payment
);
});
Core Web Vitals Optimization
Largest Contentful Paint (LCP)
Target: < 2.5 seconds
- Optimize Images: Use WebP format, lazy loading
- Reduce Server Response Time: Database query optimization, Redis
- Eliminate Render-Blocking Resources: Defer JavaScript, inline critical CSS
First Input Delay (FID)
Target: < 100 milliseconds
- Reduce JavaScript Execution: Code splitting, lazy loading
- Optimize Event Handlers: Debounce/throttle input handlers
- Use Web Workers: Offload heavy computations
Cumulative Layout Shift (CLS)
Target: < 0.1
- Reserve Space for Dynamic Content: Set explicit dimensions
- Avoid Inserting Content Above Existing: Append, don't prepend
- Use CSS Transforms: For animations instead of layout properties
/* Reserve space for payment methods */
.payment-methods {
min-height: 300px; /* Prevent layout shift when methods load */
}
/* Smooth transitions without layout shift */
.checkout-step {
transform: translateY(0);
transition: transform 0.3s ease;
}
Performance Benchmarks
Target Metrics (Production Environment)
| Metric | Target | Acceptable | Poor |
|---|---|---|---|
| Checkout Page Load (LCP) | < 1.5s | < 2.5s | > 2.5s |
| Shipping API Response | < 300ms | < 500ms | > 500ms |
| Place Order API | < 800ms | < 1.5s | > 1.5s |
| Totals Collection | < 150ms | < 300ms | > 300ms |
| Database Queries (per request) | < 20 | < 50 | > 50 |
| JavaScript Bundle Size | < 250KB | < 400KB | > 400KB |
| Time to Interactive | < 2s | < 3.5s | > 3.5s |
| Session Read/Write | < 5ms | < 15ms | > 15ms |
Testing Tools
- WebPageTest: https://www.webpagetest.org/
- Google Lighthouse: Chrome DevTools > Lighthouse
- New Relic: APM for backend performance
- Blackfire.io: PHP profiling
- Apache JMeter: Load testing
Assumptions
- Target Platform: Adobe Commerce & Magento Open Source 2.4.7+
- PHP Version: 8.2+ with OPcache enabled
- Session Storage: Redis (production requirement)
- Cache Backend: Redis or Varnish + Redis
- Database: MySQL 8.0+ with proper indexing
- Server: Nginx or Apache with HTTP/2 enabled
- Infrastructure: Adequate CPU/RAM for peak traffic
Why Performance Matters
- Conversion Rate: 1-second delay = 7% reduction in conversions
- Revenue Impact: Faster checkout directly increases sales
- User Experience: Speed is a feature that users notice
- SEO: Page speed is a ranking factor
- Competitive Advantage: Faster checkout beats competitors
Security Impact
- Caching Sensitive Data: Never cache payment information or PII
- Session Security: Redis session security configuration critical
- API Rate Limiting: Prevent abuse of performance-optimized endpoints
- Cache Poisoning: Validate cache keys to prevent manipulation
Performance vs. Security Trade-offs
- Session Locking: Required for data integrity but impacts concurrency
- Address Validation: Real-time validation vs. performance
- Payment Methods: Security checks vs. load time
- Inventory Checks: Accuracy vs. response time
Testing Performance Optimizations
- Baseline Measurement: Measure before optimization
- A/B Testing: Compare optimized vs. unoptimized
- Load Testing: Test under peak traffic conditions
- Real User Monitoring: Track actual user experience
- Continuous Monitoring: Performance regression detection
Documentation to Update
- Performance Guide: Document optimization techniques
- Infrastructure Guide: Redis, Varnish configuration
- Monitoring Guide: Setting up performance tracking
- Troubleshooting Guide: Diagnosing performance issues
- Best Practices: Performance checklist for developers