Magento 2 widgets are everywhere. The product carousel on your homepage. The "New Arrivals" CMS block. The category list sidebar. The promotional banner that changes every week. Every one of these is a widget — and every one of them can slow your store to a crawl if configured poorly.
Unlike obvious performance culprits like unoptimized images or missing Varnish caching, widgets fly under the radar. They are buried in CMS content, referenced by {{widget type="..."}} directives, or loaded via layout XML as static blocks. Their impact compounds: a single widget that runs a product collection query with no cache tags can add 200-400ms to every page load. Three of them? You just lost half a second on the critical path.
This post covers how to identify slow widgets, why they hurt performance, and what you can do about it.
How Widgets Work in Magento 2
Magento 2 widgets are small UI components rendered through a combination of:
-
Widget directives in CMS pages/blocks:
{{widget type="Magento\Catalog\Block\Product\Widget\NewWidget" ...}} - Layout XML handles that insert widget blocks into pages
- Widget instances configured in Admin → Content → Widgets
When Magento renders a page containing a widget, it:
- Parses the directive or layout instruction
- Instantiates the block class
- Runs any collection queries or data loading
- Renders the template
- Injects the output into the page
The problem is that step 3 often hits the database with unoptimized queries, and step 4 may load additional blocks recursively — all without proper full-page cache (FPC) integration.
Common Widget Performance Problems
1. Product Collection Widgets Without Caching
The Magento\Catalog\Block\Product\ListProduct widget, and any custom widget extending it, typically loads a product collection. If the block does not implement Magento\Framework\DataObject\IdentityInterface to return cache tags, Varnish and the built-in FPC cannot invalidate the widget when products change. Worse, if the block's getCacheLifetime() returns null or is not set, Magento skips block caching entirely.
2. CMS Block Widgets with Recursive Rendering
A CMS block containing a product widget, which contains another CMS block, which contains a category list widget — each level adds rendering overhead. Magento's block rendering is single-threaded and recursive; deep nesting means more block instantiation, more layout XML merging, and more template rendering time.
3. Widget Instances with Overly Broad Layout Updates
In Admin → Content → Widgets, each widget instance has a "Layout Updates" section where you assign it to specific pages and containers. A widget assigned to "All Pages" in the "Main Content Area" container will render on every single page of your store — including checkout, customer account, and static CMS pages. If that widget loads a product collection, you are running a database query on every page, even pages where the widget is not visible or relevant.
4. Static Block Widgets with Dynamic Content
The Magento\Cms\Block\Widget\Block widget renders a CMS static block. If that static block contains a widget directive (like a product list), the static block itself is cacheable, but the nested widget may not be. Magento caches the CMS block output by block ID and store view, but cache invalidation for nested widgets depends on both the outer block's and inner widget's cache tags being correctly defined.
How to Identify Slow Widgets
Enable Magento Profiler
Add ?profiler=1 to any frontend URL (requires the profiler to be enabled in app/etc/env.php):
'system' => [
'profiler' => [
'enable' => 1
]
]
Look for block rendering times under the "Blocks" section. Any block starting with widget. or cms. with a rendering time over 50ms deserves investigation.
Use Blackfire or Tideways
Profile a homepage or category page load. Look for:
- Database queries originating from
Magento\WidgetorMagento\Cmsnamespaces - Repeated collection loads from widget block classes
- Long
render()ortoHtml()calls on widget blocks
Check Widget Instance Layout Scope
Run this SQL query to find widget instances assigned to overly broad layouts:
SELECT wi.instance_id, wi.title, wpm.page_group, wpm.layout_handle
FROM widget_instance wi
JOIN widget_instance_page wpm ON wi.instance_id = wpm.instance_id
WHERE wpm.page_group = 'all_pages'
OR wpm.layout_handle = 'default';
Any widget on all_pages or default handle that loads a collection should be re-evaluated.
Fixing Widget Performance
1. Implement Proper Block Caching
Every custom widget block should implement IdentityInterface and define cache tags:
use Magento\Framework\DataObject\IdentityInterface;
use Magento\Catalog\Model\Product as ProductModel;
class MyCustomWidget extends Template implements IdentityInterface
{
public function getIdentities(): array
{
$identities = [ProductModel::CACHE_TAG];
foreach ($this->getProductCollection() as $product) {
$identities[] = ProductModel::CACHE_TAG . '_' . $product->getId();
}
return $identities;
}
public function getCacheLifetime(): ?int
{
return 3600; // Cache for 1 hour, or use null for FPC-managed lifetime
}
}
This tells Magento (and Varnish) to invalidate this block whenever any product or a specific product changes.
2. Scope Widget Instances Correctly
In Admin → Content → Widgets:
- Assign widgets to specific layout handles instead of "All Pages"
- Use "Main Content Area" only for homepage-critical widgets
- For sidebar widgets, use "Sidebar Main" or "Sidebar Additional" containers
- Remove widgets from checkout and customer account pages unless absolutely necessary
For example, a "New Arrivals" carousel should target cms_index_index (homepage), not default.
3. Replace Dynamic Widgets with Static Content Where Possible
Not every product list needs to be dynamic. If you have a "Best Sellers" widget that updates weekly, consider:
- Generating the HTML via a cron job and storing it in a CMS block
- Using a cache warmer to pre-render the widget at a set interval
- Replacing the widget with a static block for low-traffic periods and switching to dynamic only during high-traffic events
4. Limit Collection Size and Eager-Load Relations
Widget blocks that load product collections should:
- Set a strict page size limit (e.g.,
->setPageSize(8)for carousels) - Add only necessary attributes via
->addAttributeToSelect(['name', 'price', 'small_image']) - Enable flat catalog for large stores to avoid EAV joins
- Use
->addUrlRewrite()to pre-load URL rewrites and avoid N+1 queries
Example optimized collection:
$collection = $this->productCollectionFactory->create();
$collection->addAttributeToSelect(['name', 'price', 'small_image', 'url_key'])
->addAttributeToFilter('status', Status::STATUS_ENABLED)
->addAttributeToFilter('visibility', ['in' => [
Visibility::VISIBILITY_IN_CATALOG,
Visibility::VISIBILITY_BOTH
]])
->addUrlRewrite()
->setPageSize(8)
->setCurPage(1);
5. Audit CMS Blocks for Nested Widgets
Run a database query to find CMS blocks containing widget directives:
SELECT block_id, identifier, content
FROM cms_block
WHERE content LIKE '%{{widget%';
For each result, review the content. If a CMS block contains a product widget that is also used inside a widget instance, you may be double-rendering. Flatten the structure or move the widget logic entirely into the block template.
Advanced: Widget Caching with Varnish ESI
For stores with Varnish, Edge Side Includes (ESI) can help with widgets that need to be dynamic but should not block the full page cache:
- Configure the widget block with
cacheable="false"or a very short TTL - Add ESI support in your Varnish VCL for widget block URLs
- Let Varnish serve the cached page while fetching the widget content separately
However, be cautious: each ESI request adds HTTP overhead. Use ESI for high-value, truly dynamic widgets (e.g., personalized recommendations), not for static product lists.
Monitoring Widget Performance in Production
Set up alerts for:
- Pages with block rendering time > 200ms total
- Database queries from
widgetblocks exceeding 100ms - Cache hit ratio drops on pages with heavy widget usage
New Relic, Datadog, or even Magento Cloud's built-in monitoring can surface widget-level slowness if you tag your custom blocks with meaningful names during profiling.
Summary
Widgets are convenient but dangerous. Every {{widget ...}} directive is a potential performance regression. Treat them like any other code: audit, profile, cache, and scope tightly.
Key takeaways:
- Implement
IdentityInterfaceon custom widget blocks - Scope widget instances to specific layout handles
- Limit collection sizes and eager-load data
- Flatten nested CMS block → widget structures
- Monitor block rendering times in production
Your homepage does not need to query the database on every request. Fix your widgets, and your customers will notice the speed.
Top comments (0)