The smart filter starts degrading when the catalog exceeds 5,000 items. At 50,000+ items, it can take 5–15 seconds on the first request and 1–3 seconds with cache. Sound familiar? As Bitrix developers with 7+ years of experience, we've encountered this dozens of times. With 150+ projects under our belt, we guarantee results: TTFB reduction down to 200 ms. This isn't an algorithm problem — it's the consequence of the filter recalculating counters for every property value on each request. On a real project with 30 properties and 50,000 products, that means thousands of database queries per single call.
How the smart filter works under the hood
The bitrix:catalog.smart.filter component generates a form with asynchronous result loading via AJAX. The main resource consumers are COUNT queries for each property value. Without optimization, a single filter request can trigger over 200 database hits. More about the platform can be read on Wikipedia.
How to diagnose the problem
The first step is measurement, not guessing. Enable SQL profiling:
// At the beginning of the script
define('LOG_FILENAME', $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/log_filter.php');
$GLOBALS['DB']->ShowSqlStat = 'Y';
// After the filter component runs
$sqlLog = $GLOBALS['DB']->GetSqlTrack();
// Look for the slowest queries
usort($sqlLog, fn($a, $b) => $b['time'] <=> $a['time']);
var_dump(array_slice($sqlLog, 0, 10));Typical picture: 80% of time is spent on COUNT queries for recalculating the number of products per property value with the applied filter. For a deep understanding of MySQL indexing, I recommend the documentation.
Why indexes are critical
The most significant improvement without code changes is proper indexes. The smart filter extensively uses the b_iblock_element_prop_s* (simple properties) and b_iblock_element_prop_m* (multiple properties) tables.
-- Check existing indexes
SHOW INDEX FROM b_iblock_element_prop_s15;
-- 15 = infoblock ID
-- Add composite index for filtering
CREATE INDEX idx_filter_prop ON b_iblock_element_prop_s15 (IBLOCK_ELEMENT_ID, PROPERTY_186, PROPERTY_187);
-- For multiple properties table
CREATE INDEX idx_mult_prop ON b_iblock_element_property (IBLOCK_ELEMENT_ID, IBLOCK_PROPERTY_ID, VALUE_ENUM, VALUE_NUM);
After adding indexes, response time on a catalog of 30,000 items drops 2–4 times without any code change. Compare approaches:
| Approach | First request time | Cache time | Complexity |
|---|---|---|---|
| No optimization | 8–15 s | 2–3 s | — |
| Indexes only | 1–2 s | 0.5 s | Low |
| Indexes + lazy load | 0.2 s (TTFB) | 0.3 s (counters) | Medium |
| Indexes + pre-aggregation | 0.15 s | 0.15 s | High |
Note: Pre-aggregation gives a 50x gain compared to live queries on a catalog of 150,000 items.
How to configure filter caching
$APPLICATION->IncludeComponent('bitrix:catalog.smart.filter', 'my_filter', [
'IBLOCK_ID' => 15,
'SMART_FILTER' => 'Y',
'CACHE_TYPE' => 'A', // auto
'CACHE_TIME' => 3600, // 1 hour
'CACHE_GROUPS' => 'N', // do not split cache by user groups
'LAZY_LOAD' => 'Y', // lazy load counters
]);LAZY_LOAD = 'Y' — counters are loaded via a separate AJAX request after page rendering. The page responds quickly, counters appear within 200–400 ms.
Caching with invalidation tags
Aggressive caching with invalidation when the catalog changes:
// Cache tag tied to the infoblock
$taggedCache = \Bitrix\Main\Data\TaggedCache::instance();
$taggedCache->startTagCache('/catalog/filter/');
// Execute filter work
// ...
$taggedCache->registerTag('iblock_id_' . CATALOG_IBLOCK_ID);
$taggedCache->endTagCache();
// On infoblock element change — auto-invalidation:
// AddEventHandler('iblock', 'OnAfterIBlockElementUpdate', function($id, $fields) {
// \Bitrix\Main\Data\TaggedCache::instance()->clearByTag('iblock_id_' . $fields['IBLOCK_ID']);
// });
What pre-aggregation of counters gives
For catalogs with 100,000+ items — pre-aggregation into a separate table via an agent:
Pre-aggregation agent code
// Table for storing pre-aggregated data
// CREATE TABLE b_custom_filter_counts (
// iblock_id INT,
// property_code VARCHAR(50),
// property_value VARCHAR(255),
// cnt INT,
// updated_at DATETIME,
// PRIMARY KEY (iblock_id, property_code, property_value(100))
// );
class FilterCountsAgent
{
public static function recalculate(): string
{
$iblockId = CATALOG_IBLOCK_ID;
$propertyCodes = ['BRAND', 'MATERIAL', 'COUNTRY'];
foreach ($propertyCodes as $code) {
// Count via direct SQL
$sql = "
INSERT INTO b_custom_filter_counts (iblock_id, property_code, property_value, cnt, updated_at)
SELECT e.IBLOCK_ID, p.CODE, pv.VALUE, COUNT(DISTINCT e.ID), NOW()
FROM b_iblock_element e
INNER JOIN b_iblock_element_property ep ON ep.IBLOCK_ELEMENT_ID = e.ID
INNER JOIN b_iblock_property p ON p.ID = ep.IBLOCK_PROPERTY_ID
INNER JOIN b_iblock_property_enum pv ON pv.ID = ep.VALUE_ENUM
WHERE e.IBLOCK_ID = {$iblockId}
AND e.ACTIVE = 'Y'
AND p.CODE = '{$code}'
GROUP BY e.IBLOCK_ID, p.CODE, pv.VALUE
ON DUPLICATE KEY UPDATE cnt = VALUES(cnt), updated_at = VALUES(updated_at)
";
\Bitrix\Main\Application::getConnection()->query($sql);
}
return 'FilterCountsAgent::recalculate();';
}
}The agent runs every 15–30 minutes. Counters in the UI are pulled from this table — not from live queries.
Reducing the number of properties in the filter
Often 30–40% of properties in the filter are never used. Analyze data from analytics systems:
- Set up GTM events on interactions with each filter block
- Over 30 days, determine used vs unused blocks
- Remove blocks with less than 2% interaction
In practice, after such an audit, a catalog with 28 filter properties ends up with 12–15. Page generation time drops by 35–60% without technical changes.
From our practice: a marketplace with 80,000 items
A building materials marketplace. The filter responded in 8–12 seconds on the first request, 2–3 seconds with cache. The cache index was reset each time a supplier changed a price (dozens of times a day).
The solution in three steps:
- Added composite indexes on property tables — time dropped to 1.8 s without cache
- Pre-aggregation of counters via agent every 20 minutes — counters from table in 5 ms
- Lazy load counters — first byte of page at 180 ms, counters at 300 ms
Result: from 8–12 s to 180 ms TTFB, counters at 300 ms.
What's included in the optimization work?
- Audit of current filter configuration and SQL profiling
- Creation and optimization of indexes for property tables
- Caching configuration (tagged, lazy load)
- Pre-aggregation of counters (if needed)
- Recommendations for reducing unused properties
- Documentation for implementation and support
- Consultation with your team on future development
Timeline and how to start
Audit, index addition, and caching configuration — 1–2 business days. Full optimization with pre-aggregation, lazy load, and unused property audit — 3–5 business days.
We'll evaluate your project within one day. Order a filter audit — get an optimization plan. Contact us to discuss details. Leave a request for a consultation — we will analyze your catalog.

