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.
Describe the bug
Bug Report: N+1 DB Queries on WooCommerce Product Archive Pages
Plugin: RankMath SEO
File:
includes/replace-variables/class-post-variables.phpMethod:
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::setuphooks thewpaction and callssetup_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_idmeta 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
$wpdbRoot Cause
In
setup_post_variables(), thepost_thumbnailreplacement variable is registered with an eagerly-evaluated example value:get_post_thumbnail()callsget_post_thumbnail_id(), which triggers the following chain:Because
Manager::setupis hooked to thewpaction 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):
All 141 queries are thrown away. On category/archive pages, RankMath correctly uses
Term_Variablesfor OG output —Post_Variablesresults are unused.Proposed Fix
The
$this->is_post_editproperty 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 topost_thumbnailis a one-line fix: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
$this->is_post_editistrue, behaviour is identical to current'example'field is display-only in the admin snippet preview; the actual resolution callback[ $this, 'get_post_thumbnail' ]is lazy and unchangedWorkaround (in place until fix is shipped)
A
get_post_metadatafilter suppresses_thumbnail_idreads for product posts during thewpaction window and is removed atwp_headpriority 0 so that schema thumbnail resolution is unaffected. This is a workaround only and would be removed once RankMath ships the one-line fix.