Каталог на WordPress: где теряется скорость и как это измерить
Медленный каталог WordPress чаще всего упирается не в тему и не в хостинг, а в три вещи: фильтры через meta_query, подсчёт страниц и запросы в цикле. Разбираем, как это измерить и что исправлять первым.

Страница категории с 24 карточками может делать десятки лишних запросов к базе, и большинство из них ничего не добавляет к ответу. В каталоге на WordPress время обычно уходит не в тему и не в PHP, а в wp_postmeta, подсчёт страниц и повторные обращения к мета-данным в цикле. Ниже: где это происходит в WordPress 6.x, как замерить состояние до и после и что чинить первым. Готовых цифр ускорения здесь нет намеренно: они зависят от железа и объёма данных. Вместо них даём протокол замеров.
Откуда берётся время
Типичный каталог - это пользовательский тип записи или товары WooCommerce с фильтрами по цене, бренду и наличию. Каждый фильтр превращается в условия и JOIN-ы к служебным таблицам.
meta_query и таблица wp_postmeta
Таблица wp_postmeta хранит данные по схеме EAV. Индексы стоят на post_id и meta_key, но не на значении, а сам столбец meta_value имеет тип LONGTEXT. Фильтр по цене работает так: база находит все строки с ключом цены и сравнивает значения уже по месту. При type => 'NUMERIC' WordPress приводит значение через CAST, и индекс по значению в любом случае не помогает. Каждое новое условие в meta_query добавляет ещё один JOIN той же таблицы.
Пояснение: EAV (entity-attribute-value) - схема, где каждый атрибут записи лежит отдельной строкой «запись, ключ, значение». Гибко, но фильтр по нескольким атрибутам требует по одному JOIN на каждый.
Пагинация считает лишнее
Для постраничной выдачи WP_Query по умолчанию определяет общее число подходящих записей, чтобы вы могли построить навигацию. Значит, фильтр отрабатывает по всему набору, а не по 24 записям. Параметр no_found_rows => true отключает подсчёт, но вместе с ним пропадает max_num_pages. Для блоков «новинки» и «похожие» это нормально, для основного листинга нет. Конкретную форму SQL подсчёта лучше смотреть в Query Monitor на своей версии.
Вторая цена пагинации: на глубоких страницах база перебирает и отбрасывает все предыдущие строки. Страница №400 заметно дороже первой.
N+1 в цикле
По умолчанию WP_Query подгружает мета и термины для всех записей страницы одним запросом (update_post_meta_cache, update_post_term_cache). Проблемы начинаются, когда вы отключили эти кеши, а шаблон вызывает get_post_meta для каждой карточки. Или когда читаете мета у записей вне основного запроса: связанных товаров, вариантов, брендов. Тогда на каждую карточку уходит отдельный запрос.
Ускорение каталога начинается с подсчёта запросов на одну страницу, а не с выбора плагина кеширования.
Как мерить до и после
Замер без условий ничего не значит. Зафиксируйте и сохраните рядом с результатами:
- Окружение: версии PHP, WordPress и MySQL или MariaDB, число записей каталога и строк в
wp_postmeta, включён ли постоянный объектный кеш. - Отключите страничный кеш для тестового URL. Иначе вы измеряете кеш, а не код.
- Откройте страницу с плагином Query Monitor и запишите число запросов, их суммарное время, дубли и самые медленные из них.
- Снимите время до первого байта (TTFB) не менее десяти раз и возьмите медиану.
- Прогоните небольшую нагрузку и отметьте, при какой параллельности время ответа начинает расти.
- Для самых медленных запросов выполните
EXPLAINв MySQL и посмотрите, сколько строк база читает.
# TTFB одной страницы; повторите 10 раз и возьмите медиану
curl -o /dev/null -s -w '%{time_starttransfer} sec' https://example.com/catalog/
# 200 запросов, 10 одновременных (Apache Bench)
ab -n 200 -c 10 https://example.com/catalog/-- MySQL: писать в лог запросы дольше 0.5 секунды
-- нужны административные права на сервере БД, на shared-хостинге обычно недоступно
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;Таблицу результатов заведите свою: условия, число запросов, время запросов, TTFB, медиана при десяти параллельных запросах. «Типичного выигрыша» мы не приводим: любое число без вашего набора данных было бы выдумкой.
Приёмы и их цена
| Приём | Что выигрываете | Чем платите |
|---|---|---|
no_found_rows => true | Нет подсчёта всех подходящих строк | Нет max_num_pages, пагинацию делаете вручную |
fields => 'ids' | Не грузятся записи целиком | Данные карточек нужно получать отдельно |
| Кеш выдачи (transient) | Тяжёлый запрос не доходит до БД | Инвалидация, устаревшие данные до конца TTL |
| Постоянный объектный кеш (Redis, Memcached) | Кеш мета, терминов и опций между запросами | Отдельный сервис, расширение PHP, мониторинг |
| Отдельная таблица под фильтры | Фильтр по индексированным колонкам | Миграция, синхронизация, свой код поддержки |
В WooCommerce 3.6 и новее такая таблица уже есть: wc_product_meta_lookup, штатный фильтр по цене использует её. Ваши произвольные поля туда не попадают. Начиная с WordPress 6.1 WP_Query умеет кешировать результаты в объектном кеше, но без постоянного объектного кеша выигрыш ограничен одним запросом страницы (детали в документации класса).
Пример: блок с кешем выдачи
Схема для WordPress 6.x и PHP 8.x: тяжёлую выборку id кешируем, карточки подгружаем по первичному ключу.
<?php
/**
* Блок «Новинки каталога». WordPress 6.x, PHP 8.x.
*
* @return WP_Post[]
*/
function catalog_get_new_items(): array {
$cache_key = 'catalog_new_ids_v1';
$ids = get_transient( $cache_key );
if ( false === $ids ) {
$ids_query = new WP_Query( [
'post_type' => 'catalog_item',
'post_status' => 'publish',
'posts_per_page' => 12,
'orderby' => 'date',
'order' => 'DESC',
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
] );
$ids = array_map( 'intval', $ids_query->posts );
set_transient( $cache_key, $ids, HOUR_IN_SECONDS );
}
// Пустой post__in WordPress игнорирует и вернёт все записи
if ( empty( $ids ) ) {
return [];
}
// Записи и их мета подгружаются двумя запросами, а не по одному на карточку
return get_posts( [
'post_type' => 'catalog_item',
'post__in' => $ids,
'orderby' => 'post__in',
'posts_per_page' => count( $ids ),
'no_found_rows' => true,
'update_post_term_cache' => false,
] );
}
// Сбрасываем кеш при сохранении записи каталога
add_action( 'save_post_catalog_item', static function (): void {
delete_transient( 'catalog_new_ids_v1' );
} );Если в карточке выводятся термины (бренд, категория), уберите 'update_post_term_cache' => false из последнего запроса, иначе получите тот самый N+1.
Где подход ломается
- Кеш основной выдачи с фильтрами. Комбинаций слишком много, доля попаданий низкая, а без объектного кеша transient-ключи копятся в
wp_options. Кешируйте повторяющиеся блоки, а не каждую комбинацию. - Массовый импорт. Каждое сохранение вызывает
save_postи сброс кеша. На время импорта откладывайте инвалидацию до конца пакета. - Одновременный сброс. Когда TTL истёк, несколько процессов строят выдачу параллельно и нагружают базу ровно в тот момент, когда она занята.
- Индекс на
meta_value. Из-за типа LONGTEXT возможен только префиксный индекс, а числовое сравнение идёт через CAST. ПроверяйтеEXPLAIN, прежде чем рассчитывать на выигрыш. - Сторонние плагины. Дорогой запрос иногда делает плагин фильтров, а не ваш код. Query Monitor показывает компонент-источник каждого запроса.
Кеш страниц на уровне Nginx и его инвалидация - отдельная тема, здесь не разбираем.
Что делать дальше
Выберите три самые тяжёлые страницы каталога и снимите замеры по протоколу выше. Исправляйте по одному приёму за раз и повторяйте замер, иначе не поймёте, что именно помогло. В проде следите за тремя показателями: числом запросов на страницу, логом медленных запросов и долей попаданий в объектный кеш. Если после правок основная выдача всё равно упирается в фильтры по мета-полям, пора оценить отдельную таблицу под фильтры.
Аналитик звонков: Разбор записи звонка по этапам сделки: где сорвалось, что упущено, три исправляемые рекомендации с цитатами.
Спросить за 90 ₽Источники: WordPress Developer Resources: класс WP_Query, Query Monitor на WordPress.org
Частые вопросы
С какого числа товаров каталог на WordPress начинает тормозить?
У меня другая версия WordPress или WooCommerce. Что проверить?
Я добавил кеш, а страница не ускорилась. Почему?
Что может пойти не так в проде после включения кеша выдачи?
Нужен ли Redis или Memcached для каталога?
Поможет ли индекс на meta_value?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.