Skip to content

N+1 DB Queries on WooCommerce Product Archive Pages #324

@JapeNZ

Description

@JapeNZ

Describe the bug

Bug Report: N+1 DB Queries on WooCommerce Product Archive Pages

Plugin: RankMath SEO
File: includes/replace-variables/class-post-variables.php
Method: Post_Variables::setup_post_variables()
Severity: Performance — significant on any product archive with a large product count


Summary

On WooCommerce product category and archive pages, Manager::setup hooks the wp action and calls setup_post_variables() for every post in the main query. Inside that method, get_post_thumbnail() is computed eagerly as the 'example' field during variable registration. On the frontend this generates one _thumbnail_id meta read and one attachment post row read per product — none of which are ever used.

On a category page with 69 products this produces 141 unnecessary DB queries on every page load.


Environment

  • WordPress 6.x, WooCommerce 9.x
  • RankMath SEO (latest stable)
  • 69 products on the affected category page
  • Measured using a custom DB query profiler intercepting $wpdb

Root Cause

In setup_post_variables(), the post_thumbnail replacement variable is registered with an eagerly-evaluated example value:

php
$this->register_replacement(
    'post_thumbnail',
    [
        'name'        => esc_html__( 'Post Thumbnail', 'rank-math' ),
        'description' => esc_html__( 'Current Post Thumbnail', 'rank-math' ),
        'variable'    => 'post_thumbnail',
        'example'     => $this->get_post_thumbnail(), // ← evaluated immediately
        'nocache'     => true,
    ],
    [ $this, 'get_post_thumbnail' ]
);

get_post_thumbnail() calls get_post_thumbnail_id(), which triggers the following chain:

Manager::setup
  → Post_Variables::setup_post_variables()
    → get_post_thumbnail_id()
      → wp_get_attachment_image_src()
        → image_downsize()
          → wp_attachment_is_image()
            → wp_attachment_is()
              → get_post()                    ← SELECT wp_posts (×72)
              → get_attached_file()
                → get_post_meta()
                  → update_meta_cache()       ← SELECT wp_postmeta (×69)

Because Manager::setup is hooked to the wp action unconditionally, this runs for every post in the main query on every frontend page load — including archive pages where the example value is never displayed to anyone.


Profiler Evidence

Measured on a WooCommerce product category page (69 products):

Query | Count | Source -- | -- | -- SELECT * FROM wp_posts WHERE ID = ? LIMIT 1 | 72× | WP_Post::get_instance via wp_attachment_is_image SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (?) | 69× | update_meta_cache via get_post_meta Total | 141× | All from setup_post_variables loop

All 141 queries are thrown away. On category/archive pages, RankMath correctly uses Term_Variables for OG output — Post_Variables results are unused.


Proposed Fix

The $this->is_post_edit property already exists on the class for exactly this purpose, and is already used correctly for several other variables in the same method (date, modified, excerpt_only). Applying the same guard to post_thumbnail is a one-line fix:

php
// Before:
'example' => $this->get_post_thumbnail(),

// After:
'example' => $this->is_post_edit ? $this->get_post_thumbnail() : '',

This matches the established pattern in the codebase and eliminates all 141 queries on archive pages with no functional change on singular posts or in the admin editor.


Impact

  • Archive/category pages: 141 queries eliminated per page load regardless of product count (2× product count + product count in postmeta = 3× products queries)
  • Singular post pages and admin editor: No change — $this->is_post_edit is true, behaviour is identical to current
  • OG output, schema, variable resolution: Unaffected — the 'example' field is display-only in the admin snippet preview; the actual resolution callback [ $this, 'get_post_thumbnail' ] is lazy and unchanged

Workaround (in place until fix is shipped)

A get_post_metadata filter suppresses _thumbnail_id reads for product posts during the wp action window and is removed at wp_head priority 0 so that schema thumbnail resolution is unaffected. This is a workaround only and would be removed once RankMath ships the one-line fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions