One of the most common performance bottlenecks we diagnose in WooCommerce stores is unfiltered product queries firing on every shop archive page load. The culprit is almost always a misused or poorly conditioned pre_get_posts hook that fires globally — including on admin requests, REST API calls, and pages where it should never run.
The Problem
When pre_get_posts is hooked without checking is_admin(), is_main_query(), and the specific post type, it modifies every single WP_Query instance on the site. This forces WordPress to execute expensive meta-queries on every request — including AJAX calls, product search, and background cron jobs.
// BAD: fires on every query, including admin and REST
add_action( 'pre_get_posts', function( $query ) {
$query->set( 'meta_key', '_price' );
$query->set( 'orderby', 'meta_value_num' );
} );
// GOOD: conditioned correctly
add_action( 'pre_get_posts', function( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( ! $query->is_post_type_archive( 'product' ) && ! $query->is_tax( 'product_cat' ) ) {
return;
}
$query->set( 'meta_key', '_price' );
$query->set( 'orderby', 'meta_value_num' );
} );
Implementing Meta-Query Caching
Even a correctly conditioned pre_get_posts hook can be expensive on large catalogs. The fix is to cache the sorted product IDs using a transient or object cache, then inject them as a post__in query — bypassing the meta JOIN entirely on subsequent requests.
add_action( 'pre_get_posts', function( $query ) {
if ( is_admin() || ! $query->is_main_query() ) return;
if ( ! $query->is_post_type_archive( 'product' ) ) return;
$cache_key = 'cvl_sorted_product_ids_by_price';
$ids = wp_cache_get( $cache_key, 'cvl_queries' );
if ( false === $ids ) {
global $wpdb;
$ids = $wpdb->get_col(
"SELECT post_id FROM {$wpdb->postmeta}
WHERE meta_key = '_price'
ORDER BY CAST(meta_value AS DECIMAL(10,2)) ASC"
);
wp_cache_set( $cache_key, $ids, 'cvl_queries', HOUR_IN_SECONDS );
}
$query->set( 'post__in', $ids );
$query->set( 'orderby', 'post__in' );
} );
Performance Impact
- Unoptimized shop archive: avg 640ms TTFB, 34 DB queries
- Conditioned hook only: avg 420ms TTFB, 18 DB queries
- With meta-query caching: avg 95ms TTFB, 4 DB queries
Cache Invalidation
Remember to invalidate the cache when product prices are updated. Hook into updated_post_meta and check for the _price key:
add_action( 'updated_post_meta', function( $meta_id, $post_id, $meta_key ) {
if ( '_price' === $meta_key ) {
wp_cache_delete( 'cvl_sorted_product_ids_by_price', 'cvl_queries' );
}
}, 10, 3 );