WordPress · 7 мин чтения

Фильтр товаров в WooCommerce: как сделать быстрым на большом каталоге

Фильтры в WooCommerce из коробки работают ровно, пока в каталоге пара сотен товаров. Как только счётчик переваливает за 5000-10000 SKU с десятком атрибутов на карточку, стандартный виджет фильтрации начинает тянуть страницу по 2-5 секунд, а под нагрузкой кладёт базу в timeout. Я разбирал такие каталоги не один раз, и почти всегда причина одна: фильтры настроены на архитектуре, которая не рассчитана на объём, а не в самом WooCommerce как платформе.

Почему фильтры тормозят именно на большом каталоге

WooCommerce хранит произвольные поля товара по модели EAV: каждое значение атрибута или мета-поля - отдельная строка в wp_postmeta. Таблица meta_key/meta_value не рассчитана на диапазонные и множественные выборки: запрос meta_query с несколькими OR по атрибутам разворачивается в несколько self-join’ов по одной и той же таблице, и MySQL быстро упирается в полное сканирование, если значение сравнивается как число - такое приведение типа отключает индекс.

Вторая причина - подсчёт остатков по фасетам. Чтобы показать рядом с каждым значением фильтра цифру в скобках, плагину нужно на каждый оставшийся вариант выполнить отдельный COUNT-запрос с учётом уже выбранных фильтров. При 10-15 атрибутах и 5-8 значениях в каждом это легко превращается в полсотни запросов на один рендер страницы каталога.

Третья причина, самая банальная - отсутствие персистентного объектного кеша. Без Redis или Memcached WordPress хранит кеш запросов только в памяти одного PHP-процесса, так что повторный визит с тем же набором фильтров пересчитывает всё заново.

Атрибуты-таксономии вместо произвольных полей

Первое, что я проверяю на аудите - как оформлены атрибуты для фильтрации. WooCommerce поддерживает два варианта: глобальные атрибуты, которые физически живут в таксономии - wp_term_relationships и wp_term_taxonomy с готовыми индексами по object_id и term_taxonomy_id, и произвольные текстовые поля, которые часто добавляют через ACF или кастомные мета-поля прямо в постмету.

Фильтрация по таксономии идёт через tax_query и один недорогой JOIN. Фильтрация по мета-полю - через meta_query, а если полей несколько - через несколько self-join’ов по одной таблице на пару миллионов строк на крупном каталоге. На одном проекте с 12000 товаров перевод пяти произвольных полей в локальные атрибуты-таксономии снизил среднее время запроса каталога с 900 мс до 90 мс, никаких других изменений в код на тот момент не вносил.

Важно не перепутать: под фильтрацию годятся только глобальные атрибуты. Локальные хранятся в постмете _product_attributes, отдельной таксономии под них не создаётся, поэтому ни tax_query, ни стандартный виджет фильтра по атрибутам их не видят: виджет берёт список из wc_get_attribute_taxonomies() и проверяет, что такая таксономия существует. Локальный атрибут остаётся для разовых характеристик, которые нужно показать в карточке, но не тянуть в фильтры.

Таблица wc_product_meta_lookup и её индексы

С версии 3.6 WooCommerce ведёт отдельную таблицу wc_product_meta_lookup: в ней уже посчитаны min_price и max_price для вариативных товаров, остаток, средний рейтинг, количество продаж и артикул, причём с нормальными индексами под каждое поле. Родной виджет фильтра по цене и по остатку читает именно эту таблицу, а не постмету, и работает быстро сам по себе.

Проблема возникает, когда каталог заливали напрямую в базу через SQL, минуя хуки WooCommerce - тогда таблица устаревает, и фильтр по цене начинает врать или тормозить, потому что WooCommerce на лету пытается досчитать расхождение. Пересобрать таблицу можно через раздел Инструменты в статусе WooCommerce либо из консоли:

wp wc tool run regenerate_product_lookup_tables --user=1

На каталоге в 20000 товаров такая пересборка занимала около 12 минут в фоне через Action Scheduler, магазин при этом продолжал принимать заказы.

Если нужны собственные фасеты сверх стандартных полей, например фильтр по бренду как отдельная колонка, а не таксономия, логичнее завести свою таблицу-надстройку по тому же принципу: синхронизировать её на хуках woocommerce_new_product и woocommerce_update_product, а не тянуть значение из постметы на каждый запрос.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Готовые плагины для фильтрации товаров: что я реально сравнивал

Писать фасетный фильтр с нуля имеет смысл только под очень специфичную выдачу. В большинстве проектов беру готовый плагин и донастраиваю индексы под него. Из тех, что реально держал на боевых каталогах:

Плагин Как считает фасеты Поведение на каталоге от 15000 товаров
FacetWP Строит собственный индекс в отдельных таблицах при сохранении товара Фильтр отдаётся из индекса, а не из постметы - на практике самый стабильный вариант на крупных каталогах
Встроенные блоки WooCommerce (Filter by Attribute и подобные) Считает фасеты на лету через tax_query при каждом запросе Работает нормально при атрибутах-таксономиях и включённом Redis, без кеша заметно тормозит после нескольких тысяч SKU
YITH WooCommerce Ajax Product Filter Комбинирует meta_query и tax_query в зависимости от типа поля Требует ручной проверки, что фильтруемые поля переведены в атрибуты, иначе тянет за собой meta_query по постмете

FacetWP платный, но именно предпостроенный индекс снимает основную боль - фасеты не пересчитываются на каждый визит, а обновляются при сохранении товара. Для каталогов без бюджета на лицензию комбинация из атрибутов-таксономий, встроенных блоков и Redis в большинстве случаев вытягивает 10000-15000 товаров без сторонних плагинов вообще.

Кеширование фильтрованных выдач и AJAX-запросов

Даже с быстрыми запросами не стоит пересчитывать одно и то же на каждый визит. Что использую на практике:

  • персистентный объектный кеш (Redis или Memcached) вместо кеша в памяти процесса - включаю первым делом на любом WooCommerce с каталогом от пары тысяч товаров;
  • фрагментное кеширование блока с фасетами и счётчиками отдельно от списка товаров - счётчики можно пересчитывать по крону раз в 5-15 минут для крупного каталога, а не на каждый запрос;
  • полностраничный кеш (fastcgi_cache в Nginx, Varnish или кеш на уровне хостинга) для анонимных посетителей без активной корзины, со сбросом по вебхуку при изменении остатка или цены - похожий паттерн использую при синхронизации остатков со СДЭК и приёме оплаты через T‑Bank, там событие изменения остатка тоже должно долетать до кеша, а не жить своей жизнью.

Отдельно слежу за комбинаторным взрывом: если в каталоге 10 атрибутов по 6 значений, сочетаний фильтров формально десятки тысяч, и кешировать каждую комбинацию бессмысленно. Кеширую отдельно тяжёлую часть - подсчёт остатков по фасетам, а лёгкую выборку товаров под уже выбранные фильтры отдаю на прямой запрос к базе с нормальными индексами.

Когда переходить на Elasticsearch или Algolia

На каталогах от 20000-30000 товаров с активным трафиком даже правильно настроенный MySQL начинает упираться в саму природу задачи: фасетный подсчёт по нескольким измерениям одновременно - это по сути многомерный GROUP BY, и никакие индексы не убирают экспоненциальный рост числа сочетаний. В этот момент имеет смысл выносить поиск и фильтрацию в отдельный движок - ElasticPress поверх self-hosted Elasticsearch или OpenSearch, либо облачный Algolia или Meilisearch.

Синхронизация каталога с индексом идёт на тех же хуках сохранения товара, задержка обычно в пределах нескольких секунд - для остатков и цены это приемлемо, для критичных по времени промоакций стоит форсировать синхронизацию отдельным вызовом. Обратная сторона - отдельный сервер под поисковый движок или подписка на облачный сервис и дополнительный слой, который нужно мониторить. До 15000-20000 товаров смысла в этом усложнении обычно нет: разница в скорости не окупает эксплуатационные расходы.

Что я проверяю на аудите фильтра каталога

Порядок действий, который использую на реальных проектах, независимо от того, кастомная тема или готовая:

  1. Профилирую страницу каталога через Query Monitor - смотрю список запросов, их время и повторяющиеся паттерны (частый признак N+1 - десятки одинаковых запросов на разные ID товаров).
  2. Проверяю размер wp_postmeta относительно wp_posts - на запущенных каталогах после нескольких лет импортов постмета разрастается в 5-10 раз, там же обычно и мусор от старых плагинов.
  3. Перевожу произвольные поля, участвующие в фильтрации, в атрибуты-таксономии там, где это оправдано по смыслу.
  4. Проверяю актуальность wc_product_meta_lookup и индексы под собственные кастомные поля.
  5. Включаю персистентный объектный кеш и фрагментное кеширование фасетов.
  6. Прогоняю нагрузочный тест на 50-100 одновременных сессий с разными комбинациями фильтров перед сдачей работы.

Если структуру атрибутов и индексов переписывать самому некогда или страшно трогать боевую базу без подстраховки, обычно этим занимаюсь я отдельной задачей - доработкой существующего сайта: на каталоге в 10000-20000 товаров разница в скорости фильтра становится заметна визуально уже после первого прохода, без переезда на другую платформу.

Частые вопросы

Сколько товаров считается большим каталогом для WooCommerce?

Жёсткой границы нет, но по моей практике проблемы с фильтрами начинают вылезать уже на 3000-5000 товарах, если атрибуты хранятся как произвольные мета-поля, а не таксономии. При правильной архитектуре - атрибуты-таксономии, актуальный wc_product_meta_lookup, персистентный кеш - каталог в 15000-20000 товаров держится на обычном MySQL без сторонних поисковых движков.

Нужно ли переходить на Elasticsearch, если товаров всего пара тысяч?

Нет, на таком объёме это лишний слой инфраструктуры без ощутимого выигрыша. На 1000-3000 товарах узкое место почти всегда не в базе, а в отсутствии индексов и объектного кеша - это дешевле и быстрее чинить, чем поднимать отдельный поисковый сервер.

Почему стандартный фильтр по атрибутам иногда тормозит даже на 1000-2000 товарах?

Чаще всего это отсутствие персистентного объектного кеша в связке с тяжёлой темой, которая на каждый запрос каталога выполняет собственные дополнительные запросы к постмете помимо стандартных WooCommerce. Второй частый вариант - атрибуты добавлены как произвольные текстовые поля через ACF, а не как атрибуты товара.

Как проверить, что именно тормозит фильтр на конкретном сайте?

Ставлю Query Monitor, открываю страницу каталога с активными фильтрами и смотрю вкладку с запросами к базе: сортирую по времени выполнения и по количеству повторов одного и того же запроса. Обычно топ‑3 самых медленных запроса сразу показывают, где узкое место - meta_query по постмете, отсутствующий индекс или пересчёт остатков по фасетам.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно