DEV Community

Magevanta
Magevanta

Posted on • Originally published at magevanta.com

Magento 2 Widget Performance Optimization: Fix Slow CMS Blocks & Homepage Rendering

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:

  1. Parses the directive or layout instruction
  2. Instantiates the block class
  3. Runs any collection queries or data loading
  4. Renders the template
  5. 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
    ]
]
Enter fullscreen mode Exit fullscreen mode

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\Widget or Magento\Cms namespaces
  • Repeated collection loads from widget block classes
  • Long render() or toHtml() 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';
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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%';
Enter fullscreen mode Exit fullscreen mode

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:

  1. Configure the widget block with cacheable="false" or a very short TTL
  2. Add ESI support in your Varnish VCL for widget block URLs
  3. 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 widget blocks 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 IdentityInterface on 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)