The wishlist is one of the most under-profiled features in Magento 2. It's always there, nobody thinks about it, and then one day a customer with a 60-item wishlist opens their account page and the server takes four seconds to render it. Worse: the sidebar widget loads a version of that same data on every page for logged-in customers.
This post breaks down exactly what happens when Magento renders a wishlist, where the N+1 queries hide, how to measure the damage, and which fixes actually move the needle.
Why the Wishlist Is Expensive by Design
A wishlist item isn't just a row in wishlist_item. When Magento renders the wishlist page, it builds a collection of wishlist items joined with product data, and then hands each item to a chain of column blocks (Image, Info, AddToCart, Price, Remove, ...). Each of those blocks talks to product services — per item.
The naive query flow for a wishlist with N items looks like this:
-
1 query — base
wishlist_itemcollection, join oncatalog_product_entityand store-level attributes -
N queries — stock / salable checks (each item's
isSalable()resolves because the Add to Cart button needs it) - N queries — final price lookups, including catalog price rule checks per item
- N queries — option and image/media gallery lookups
- N queries — additional attribute loads per item (when attributes weren't joined in the collection)
On a 50-item wishlist, you're easily looking at 150–300+ queries for a single page. With configurable products in the list, the configured variant gets resolved per item too, pushing the count toward 500+.
If you've read the inventory reservation post, you already know where part of this goes: every salable check touches inventory_reservation with a SUM(quantity) for that SKU and stock. Multiply that by N items, and that "innocent" wishlist page is hammering one of the largest tables in your database.
The Real Culprit You're Ignoring: The Sidebar
Before optimizing the wishlist page itself, check whether you're paying the wishlist tax on every page.
Magento's default layout registers the wishlist sidebar block (Magento\Wishlist\Block\Customer\Sidebar, block name wishlist_sidebar) in the sidebar.additional container — which renders on every page for logged-in customers. Its template loads the customer's wishlist item collection to show the last items. In other words:
A customer with a big wishlist triggers a wishlist item collection load (with product joins) on every single page view.
If your logged-in traffic is meaningful, profile a simple account/shopping page: you'll often find wishlist-related queries on requests that have nothing to do with wishlists. The header wishlist counter (the wishlist customer data section) does the same thing — it loads the full collection just to count items.
How to Measure the Damage
Don't guess. Profile it:
bin/magento dev:query-log:enable
Then open the wishlist page as a logged-in customer and grep for the wishlist signature:
grep "wishlist_item" var/log/query.log | wc -l
grep "inventory_reservation" var/log/query.log | wc -l
grep "catalog_product_entity" var/log/query.log | wc -l
For a proper breakdown, use a profiler (Blackfire, Xdebug + Webgrind) and count the queries per wishlist item. A healthy wishlist page with 20 items should render in well under 100 queries. If you're at 10+ queries per item, you have N+1 patterns to kill.
Also check your indexers:
bin/magento indexer:status
If catalog_product_price, catalogrule_product or inventory are in Update by Schedule mode with a backlog, the wishlist page (and anything else that resolves product state per item) gets dramatically slower, because it falls back to computing those states on the fly.
The Fixes, Ranked by Impact
1. Kill the Sidebar (if you're not using it)
Most stores don't rely on the sidebar wishlist widget. Remove it and eliminate the per-page tax entirely. In your theme's layout:
<!-- app/design/frontend/<Vendor>/<Theme>/Magento_Wishlist/layout/default.xml -->
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd">
<body>
<referenceBlock name="wishlist_sidebar" remove="true"/>
</body>
</layout>
2. Replace the Header Counter with a Light COUNT
If you want to keep the counter badge, don't load the whole collection for it. The Magento\Wishlist\CustomerData\Wishlist section source loads all items just to count them. Swap it for a cheap count query via a di.xml preference or a plugin:
// Count only: SELECT COUNT(*) FROM wishlist_item WHERE wishlist_id = ? AND store_id = ?
One indexed COUNT beats a multi-join collection load on every request.
3. Batch the Stock Checks
The Add to Cart column calls isSalable() per item, which triggers the MSI salable-qty pipeline per SKU — including the inventory_reservation SUM. Instead of fixing the core (which is risky), you can pre-resolve salability for all items in one pass with a plugin on the wishlist block or a custom block override, using the batch API:
$stockStatuses = $this->getStockStatusBySkus->execute($skus, $stockId);
Loading stock status for 50 SKUs in one request is a handful of queries instead of 50 full salable checks.
4. Keep the Price Indexers Warm
The per-item price block (FinalPrice → CatalogRulePrice) reads catalogrule_product_price. When catalog rules are active and the index is stale or missing rows, Magento computes rule prices per product on the spot — a classic N+1 multiplier. Ensure:
-
catalog_product_priceandcatalogrule_productrun on schedule (or manually after rule changes) - The price index is actually up to date before traffic hours, not "scheduled" with a 2-day backlog
5. Paginate and Limit
The wishlist page default limit is generous. If your customers build huge wishlists, reduce the per-page item count (Layout/UI component or wishlist/general pagination settings). Fewer items per request = fewer per-item queries, and customers rarely scroll 100 wishlist items.
6. Warm Up Product Images
Each wishlist item renders a product thumbnail. On the first visit (or after a cache flush), Magento generates those images on the fly — N image generations during a page request is a request-time killer. Run a warmup in the deploy or cron:
bin/magento catalog:images:resize
7. Revisit Configurable-Product Items
Configurable products in a wishlist force Magento to resolve the selected child product per item (price, image, options). If configurable wishlist usage is heavy, consider a custom renderer that reuses the already-loaded options data instead of re-loading the child product per item. This is the biggest single-page win when your wishlists are dominated by configurable SKUs.
A Note on Table Growth
wishlist_item and wishlist_item_option accumulate leftovers: items removed from the list leave orphaned option rows, and abandoned lists persist. On very large installs:
-- Orphaned options from removed items
SELECT COUNT(*) FROM wishlist_item_option o
LEFT JOIN wishlist_item i ON o.wishlist_item_id = i.wishlist_item_id
WHERE i.wishlist_item_id IS NULL;
Clean those up periodically (off-peak, in batches) and ensure wishlist_item.wishlist_id is indexed — with multi-store setups, also confirm store_id filtering isn't forcing scans.
The Checklist
- [ ] Remove
wishlist_sidebarunless actively used - [ ] Replace the header counter with a COUNT query
- [ ] Batch stock/salable resolution instead of per-item
isSalable() - [ ] Keep
catalog_product_price/catalogrule_productindexes current - [ ] Reduce default wishlist pagination for big lists
- [ ] Warm up product images after deploys
- [ ] Profile before/after: query count per item should drop to < 2–3
- [ ] Schedule cleanup of orphaned
wishlist_item_optionrows
Summary
The wishlist is a textbook N+1 machine: one collection load multiplied by per-item stock, price, image, and option lookups — and the sidebar multiplies that tax across every page for logged-in customers. The good news is the fixes are surgical: remove the sidebar, lighten the counter, batch the stock checks, keep indexes warm, and paginate. None of them touch checkout or core business logic, so the risk/reward ratio is excellent.
Profile first, kill the biggest multiplier first (usually the sidebar), and re-measure. A wishlist page that renders in 400 queries can comfortably drop to 50 — and your logged-in customers will feel it on every page, not just the wishlist.
Top comments (0)