info

Affiliate Disclosure: This article contains benchmarked tools that may compensate us. We only recommend hardware and software tested in our lab environments for maximum LCP and INP efficiency.

Unfiltered Product Queries: Why pre_get_posts Is Slowing Your Shop Archive

Learn why pre_get_posts is slowing down your shop archive page and how to implement proper meta-query caching.

calendar_today folder

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 );