Спершу профілювання через Query Monitor, потім persistent object cache у Redis, правильні аргументи WP_Query, індекси або власні таблиці замість meta_query і повне кешування сторінок на рівні nginx.
Як питають
Чому сторінка каталогу з фільтрами відкривається 6 секунд?
Чим transients відрізняються від object cache?
З чого почати оптимізацію WooCommerce на 50 000 товарів?
продуктивністьobject cacheWP_Query
Пояснення
Оптимізація починається з вимірювання. Query Monitor показує кожен SQL-запит сторінки з часом і стеком викликів, і в каталозі майже завжди винен один тип запиту: WP_Query з meta_query по кількох ключах. Таблиця wp_postmeta має EAV-структуру, кожен ключ у фільтрі додає JOIN на її копію, а індексу по meta_value немає. На 50 000 товарів такий запит триває секунди.
Далі йдуть три рівні. Перший — persistent object cache у Redis: без нього WordPress на кожен запит перечитує опції, мету й терми, а transients живуть у wp_options і самі стають повільними запитами. Другий — правильні аргументи WP_Query: no_found_rows там, де не потрібна пагінація, fields => ids для списків, вимкнені update_post_meta_cache і update_post_term_cache, коли мета не читається. Третій — структура даних: атрибути з обмеженим набором значень переносяться в таксономії, числові діапазони й багатовимірні фільтри у власну таблицю з індексами або в зовнішній пошуковий рушій.
Останній шар — повне кешування сторінок на nginx або Varnish для всього, що не персоналізоване, з інвалідацією по save_post. Кошик, чекаут і кабінет із цього кешу виключаються, а їхні дорогі фрагменти кешуються через wp_cache з контекстом у ключі.
КодPHP
// Повільно: meta_query по трьох ключах = три JOIN на wp_postmeta без індексу по meta_value
$q = new WP_Query([
'post_type' => 'product',
'meta_query' => [
['key' => '_color', 'value' => 'red'],
['key' => '_size', 'value' => 'M'],
['key' => '_price', 'value' => [100, 500], 'compare' => 'BETWEEN', 'type' => 'NUMERIC'],
],
]);
// Швидше: атрибути в таксономіях, лише id, без підрахунку found_rows і зайвих кешів
$ids = (new WP_Query([
'post_type' => 'product',
'fields' => 'ids',
'posts_per_page' => 24,
'no_found_rows' => true, // без SQL_CALC_FOUND_ROWS
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'tax_query' => [
['taxonomy' => 'pa_color', 'field' => 'slug', 'terms' => 'red'],
['taxonomy' => 'pa_size', 'field' => 'slug', 'terms' => 'm'],
],
]))->posts;
// Дорогий результат у object cache (Redis), а не в wp_options
$facets = wp_cache_get('catalog_facets', 'shop');
if ($facets === false) {
$facets = shop_build_facets();
wp_cache_set('catalog_facets', $facets, 'shop', 10 * MINUTE_IN_SECONDS);
}
Що хоче почути інтервʼюер
Що починаєте з вимірювання: Query Monitor або New Relic показують найповільніший запит, а не здогадки про кеш-плагіни.
Що головний ворог каталогу це meta_query по кількох ключах: wp_postmeta з EAV-структурою робить JOIN на кожен ключ і не має індексу по значенню.
Що persistent object cache у Redis чи Memcached кешує результати wp_cache і get_option між запитами, без нього WordPress перечитує опції та мету щоразу.
Що аргументи WP_Query мають значення: no_found_rows, fields => ids, update_post_meta_cache і update_post_term_cache вимкнені там, де не потрібні.
Що для фільтрів каталогу масштабується лише власна таблиця з індексами або зовнішній пошуковий рушій, а повне сторінкове кешування знімає навантаження з усього, що не персоналізоване.
Типові помилки
Ставити ще один кеш-плагін замість пошуку повільного запиту.
Плутати transients і object cache: transient без persistent cache живе в wp_options і сам стає повільним запитом.
Використовувати posts_per_page => -1 і потім фільтрувати в PHP.
Робити фільтри каталогу через meta_query з пʼятьма ключами й дивуватись JOIN на пʼять копій wp_postmeta.
Кешувати сторінку цілком разом із кошиком чи іменем користувача й показувати чужі дані.
ПОРАДА
Скажіть, що починаєте з профілювання Query Monitor, а не з встановлення кеш-плагіна навмання. І назвіть, який саме запит зазвичай виявляється винним.
Додаткові питанняЗ ВІДПОВІДЯМИ
Transient це API з часом життя: set_transient зберігає значення, і воно переживає запит. Без persistent object cache transients лежать у wp_options, і кожен get_transient це запит у базу, а autoload-опції ще й роздувають памʼять. З увімкненим Redis transients автоматично перенаправляються в object cache. wp_cache_* без persistent бекенду живе лише в межах одного запиту.
Query Monitor показує всі SQL-запити з часом, кількістю рядків і стеком викликів, звідки видно, який плагін або шаблон їх породив. Для продакшену без плагінів увімкнути slow_query_log у MySQL з порогом 0.5 с або SAVEQUERIES з логуванням у окремий файл. Далі EXPLAIN на знайденому запиті.
Атрибути з обмеженою кількістю значень виносити в таксономії, tax_query працює через індексовані таблиці term_relationships. Числові діапазони або багато атрибутів зберігати у власній таблиці product_index з індексами і фільтрувати JOIN до неї через фільтр posts_join або власний запит з fields => ids. У WooCommerce для цього є wc_product_meta_lookup, а для великих каталогів зовнішній рушій: Elasticsearch через ElasticPress або Meilisearch.
Сторінки без персоналізації: каталог, картки товарів, статті — через nginx fastcgi_cache або Varnish з інвалідацією по save_post. Кошик, чекаут, кабінет, будь-що з cookie сесії кешувати не можна; для них залишають object cache і кешування фрагментів через wp_cache з ключем, у який входить контекст.