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 schedule — min read folder
format_list_bulleted

Table of Contents

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

Get Weekly Performance Benchmarks

Join 2,400+ WordPress engineers. Real lab data, no fluff.

No spam. Unsubscribe anytime.

engineering

About the Lab & Author

This benchmark and optimization guide was written by the Performance Engineering team at CoreVitalsLab. With over a decade of experience optimizing high-traffic WooCommerce stores and enterprise WordPress deployments, we focus exclusively on data-driven TTFB reduction, caching architecture, and passing Core Web Vitals.