DEV Community

Magevanta
Magevanta

Posted on • Originally published at magevanta.com

Magento 2 UI Component Performance: Optimizing Admin Grids & Data Providers

Magento 2 UI components are the backbone of every admin grid you've ever used. The product grid. The order grid. The customer grid. The invoice grid. Every page in the admin panel that shows a table of data with filtering, sorting, and pagination is powered by the UI component system.

And they are slow.

Not "a little slow" — painfully slow on stores with 50,000+ products, 100,000+ orders, or multi-website setups with complex ACL. A product grid that takes 8 seconds to load is not uncommon. A customer grid that times out on filter? Also common. The irony is that the admin panel is where your team spends hours every day, and every second of grid load time is a second of lost productivity.

This post covers why UI components are slow, how to profile them, and what you can do to make them fast again.

How Magento 2 UI Components Work

A UI component is a declarative XML configuration that Magento renders into a knockout.js-based frontend component. The typical flow is:

  1. XML Declarationview/adminhtml/ui_component/product_listing.xml defines columns, filters, bookmarks, and the data provider
  2. Data Provider — A PHP class implementing Magento\\Framework\\View\\Element\\UiComponent\\DataProvider\\DataProviderInterface that fetches data from the database
  3. AJAX Endpoint — The grid loads data via an AJAX call to mui/index/render/ which returns JSON
  4. Knockout.js Rendering — The frontend renders the grid client-side using knockout templates

The performance problems live in steps 2 and 3. The data provider is where database queries run, and the AJAX endpoint is where Magento bootstraps the entire application just to serve a JSON response.

The Core Performance Problems

1. Data Provider Collection Loading

Every UI component grid uses a Magento\\Framework\\View\\Element\\UiComponent\\DataProvider\\CollectionFactory to build a collection. For the product grid, this is Magento\\Catalog\\Ui\\DataProvider\\Product\\ProductCollection. The collection loads products with EAV joins, inventory data, price indexing data, and store-specific attributes.

The problem: the collection loads all EAV attributes for every product in the grid, even though only a handful are displayed as columns. A product grid showing 20 products with 200 EAV attributes means 4,000 EAV joins — most of which are never rendered.

2. Full Page Bootstrap on Every AJAX Call

When the grid requests data, it hits mui/index/render/. This route bootstraps the entire Magento application: config loading, area code resolution, plugin interception, ACL checks, translation loading, layout generation. For a grid that paginates through 50 pages, that's 50 full Magento bootstraps — each one adding 200-500ms of overhead before the actual query runs.

3. Bookmark Management Overhead

Magento saves grid state (filters, sorting, pagination) as "bookmarks" in the ui_bookmark table. Every grid interaction — changing a filter, sorting a column, navigating pages — triggers a bookmark save operation. On multi-admin setups with many users, the bookmark table grows rapidly and queries against it slow down.

4. AddColumn After Data Loading

Many third-party modules add columns to grids via plugins on getColumns() or by modifying the UI component XML. If a column requires additional data (e.g., a custom attribute from a third-party table), the data is often loaded per-row after the main collection has already been loaded. This is the classic N+1 problem: 20 products in a grid page means 20 additional queries.

5. Mass Action Preload

When an admin user selects "Select All" in a grid, Magento needs to know all matching IDs — not just the current page. Some grids handle this by loading the full collection without LIMIT. On a store with 200,000 products, this means loading 200,000 product IDs into memory, serializing them, and sending them to the browser as a JavaScript variable.

Profiling UI Component Performance

Enable Database Query Logging

Add this to app/etc/env.php under the db section:

'db' => [
    'connection' => [
        'default' => [
            'debug' => true,
            'log_path' => '/var/log/magento_db.log'
        ]
    ]
]
Enter fullscreen mode Exit fullscreen mode

Load the product grid and check the log. You'll see dozens of queries per grid load — many of them EAV joins. Look for:

  • Queries with JOIN ...catalog_product_entity_varchar repeated dozens of times
  • COUNT(*) queries that scan the full product table
  • SELECT * FROM ui_bookmark queries that are slower than expected

Use Magento Profiler

Enable the profiler in app/etc/env.php:

'system' => [
    'profiler' => [
        'enable' => 1
    ]
]
Enter fullscreen mode Exit fullscreen mode

Append ?profiler=1 to any admin grid URL. Look for:

  • Magento\\Framework\\View\\Element\\UiComponent\\DataProvider\\DataProvider — time spent in data provider
  • Magento\\Ui\\Model\\Bookmark — bookmark management overhead
  • Magento\\Framework\\Api\\SearchCriteriaBuilder — search criteria processing

Check AJAX Response Time

Open Chrome DevTools → Network tab. Load the product grid. Look for the mui/index/render/ request. The total time is your grid load time. If it's over 2 seconds, you have a problem. If it's over 5 seconds, you have a serious problem.

Optimization Strategies

1. Limit EAV Attributes in the Collection

The most impactful optimization is limiting which EAV attributes the product grid collection loads. By default, Magento loads all attributes that are configured as "Used in Grid" (is_used_in_grid = 1 in catalog_eav_attribute). Many of these are unnecessary.

Check which attributes are flagged for grid use:

SELECT attribute_id, is_used_in_grid
FROM catalog_eav_attribute
WHERE is_used_in_grid = 1;
Enter fullscreen mode Exit fullscreen mode

Disable grid loading for attributes that don't appear in the grid:

UPDATE catalog_eav_attribute
SET is_used_in_grid = 0
WHERE attribute_id IN (
    SELECT attribute_id FROM eav_attribute
    WHERE attribute_code IN ('description', 'short_description', 'meta_title', 'meta_description')
);
Enter fullscreen mode Exit fullscreen mode

This alone can reduce grid load time by 30-50% on stores with many EAV attributes.

2. Optimize the Data Provider

Create a custom data provider that extends the default one and optimizes the collection:

<?php
namespace MageVanta\UiPerformance\Ui\DataProvider;

use Magento\Catalog\Ui\DataProvider\Product\ProductCollection;
use Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider;

class OptimizedProductDataProvider extends ProductDataProvider
{
    public function getData()
    {
        // Only load attributes that are actually displayed as columns
        $this->getCollection()
            ->addAttributeToSelect(['name', 'sku', 'price', 'status', 'visibility']);

        // Add index hints for large catalogs
        $this->getCollection()->getSelect()
            ->reset(\Magento\Framework\DB\Select::ORDER)
            ->order('entity_id DESC');

        return parent::getData();
    }
}
Enter fullscreen mode Exit fullscreen mode

Reference it in your product_listing.xml:

<dataSource name="product_listing_data_source">
    <dataProvider class="MageVanta\UiPerformance\Ui\DataProvider\OptimizedProductDataProvider" name="product_listing_data_source">
        <settings>
            <requestFieldName>id</requestFieldName>
            <primaryFieldName>entity_id</primaryFieldName>
        </settings>
    </dataProvider>
</dataSource>
Enter fullscreen mode Exit fullscreen mode

3. Add Database Indexes for Grid Filters

The most common grid filters — status, visibility, type_id, attribute_set_id — need proper indexes. Check your slow query log for queries on catalog_product_entity_int with filter conditions:

-- Add composite index for common grid filter combinations
ALTER TABLE catalog_product_entity_int
ADD INDEX idx_grid_filter (attribute_id, store_id, value);

-- Index for the entity_id join used in every grid load
ALTER TABLE catalog_product_entity_int
ADD INDEX idx_entity_attribute (entity_id, attribute_id, store_id);
Enter fullscreen mode Exit fullscreen mode

4. Implement Server-Side Pagination Correctly

Magento's UI component system supports server-side pagination, but some custom grids break this by loading the full collection. Ensure your data provider respects the pagination parameters:

public function getData()
{
    $collection = $this->getCollection();

    // Respect page size from search criteria
    $searchCriteria = $this->getSearchCriteria();
    $collection->setPageSize($searchCriteria->getPageSize());
    $collection->setCurPage($searchCriteria->getCurrentPage());

    $items = [];
    foreach ($collection->getItems() as $item) {
        $items[] = $item->toArray();
    }

    return [
        'totalRecords' => $collection->getSize(),
        'items' => $items
    ];
}
Enter fullscreen mode Exit fullscreen mode

The key insight: $collection->getSize() runs a COUNT(*) query without loading all items, while $collection->getItems() only loads the current page. Never call getItems() before setting the page size.

5. Cache Bookmark Queries

The ui_bookmark table is queried on every grid render. Add a cache layer:

<?php
namespace MageVanta\UiPerformance\Model;

use Magento\Ui\Model\BookmarkRepository;
use Magento\Framework\App\CacheInterface;

class CachedBookmarkRepository extends BookmarkRepository
{
    const CACHE_KEY_PREFIX = 'ui_bookmark_';
    const CACHE_TTL = 3600;

    private $cache;

    public function __construct(
        BookmarkRepository $subject,
        CacheInterface $cache
    ) {
        $this->cache = $cache;
    }

    public function getListByUserId($userId)
    {
        $cacheKey = self::CACHE_KEY_PREFIX . $userId;
        $cached = $this->cache->load($cacheKey);
        if ($cached) {
            return json_decode($cached, true);
        }

        $result = parent::getListByUserId($userId);
        $this->cache->save(json_encode($result), $cacheKey, ['ui_bookmark'], self::CACHE_TTL);
        return $result;
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Replace the AJAX Endpoint with a Custom Route

The mui/index/render/ endpoint bootstraps the full Magento application on every request. For high-traffic admin panels, create a lightweight REST endpoint that skips layout generation and area code overhead:

<?php
namespace MageVanta\UiPerformance\Controller\Grid;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Json\Helper\Data as JsonHelper;
use MageVanta\UiPerformance\Model\GridDataResolver;

class Data extends Action
{
    private $jsonHelper;
    private $gridDataResolver;

    public function execute()
    {
        $namespace = $this->getRequest()->getParam('namespace');
        $data = $this->gridDataResolver->resolve($namespace, $this->getRequest()->getParams());

        return $this->jsonHelper->jsonEncode($data);
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach skips the layout XML processing that mui/index/render/ performs, saving 100-200ms per request.

7. Optimize Mass Action Handling

Instead of loading all matching IDs into the browser, implement a server-side filter storage for mass actions:

public function execute()
{
    $filter = $this->getRequest()->getParam('filter');
    $filterHash = md5(json_encode($filter));

    // Store filter criteria in a temporary table
    $this->filterStorage->save($filterHash, $filter, 3600);

    // Return just the hash — not 200,000 IDs
    return $this->jsonResponse(['filter_hash' => $filterHash]);
}
Enter fullscreen mode Exit fullscreen mode

When the mass action is executed, the backend reads the filter from storage and applies it to the collection — no need to send all IDs to the browser.

Monitoring UI Component Performance

Add a plugin to Magento\\Ui\\Controller\\Index\\Render to log grid load times:

public function aroundExecute($subject, $proceed)
{
    $start = microtime(true);
    $result = $proceed();
    $duration = round((microtime(true) - $start) * 1000);

    if ($duration > 2000) {
        $this->logger->warning(sprintf(
            'Slow UI component render: %s took %dms',
            $this->getRequest()->getParam('namespace'),
            $duration
        ));
    }

    return $result;
}
Enter fullscreen mode Exit fullscreen mode

Set up an alert for any grid load exceeding 3 seconds. On a well-optimized store, the product grid should load in under 1.5 seconds, and the order grid in under 1 second.

The Bottom Line

Magento 2 UI components are powerful but inefficient by default. The combination of EAV overhead, full application bootstrap on every AJAX call, and unoptimized data providers creates a perfect storm of admin panel slowness.

The most impactful changes you can make:

  1. Reduce EAV attribute loading — disable is_used_in_grid for unused attributes
  2. Add proper database indexes — on catalog_product_entity_int for grid filter columns
  3. Cache bookmark queries — the ui_bookmark table is hit on every grid render
  4. Replace the default AJAX endpoint — skip layout generation for grid data requests
  5. Implement server-side mass action filters — never send all IDs to the browser

On a store with 100,000+ products, these optimizations typically reduce product grid load time from 6-8 seconds to under 2 seconds. That's a 4x improvement in admin panel responsiveness — and your team will feel it immediately.

Top comments (0)