Commit ceea22a27f5 for woocommerce
commit ceea22a27f53820052e428a1381374cf2c750ddc
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date: Tue Mar 24 12:54:32 2026 +0100
[dev] Performance: introduce new skill for using option cache priming (reviews and code generation) (#63720)
diff --git a/.ai/skills/woocommerce-performance/SKILL.md b/.ai/skills/woocommerce-performance/SKILL.md
index b837faba07a..b9daf4ed925 100644
--- a/.ai/skills/woocommerce-performance/SKILL.md
+++ b/.ai/skills/woocommerce-performance/SKILL.md
@@ -1,10 +1,14 @@
---
name: woocommerce-performance
-description: Identify missing _prime_post_caches calls in WooCommerce PHP code. Use when writing or reviewing code that loads collections of post-based objects (products, orders) or renders product lists with images.
+description: Identify missing cache priming calls in WooCommerce PHP code. Use when writing or reviewing code that loads collections of post-based objects (products, orders), renders product lists with images, or reads multiple options in a loop or method.
---
# WooCommerce Performance
-## Cache Priming — [cache-priming.md](cache-priming.md)
+## Post Cache Priming — [cache-priming.md](cache-priming.md)
Use when writing or reviewing code that loads collections of post-based objects (products, orders) or renders product lists with images. Covers `_prime_post_caches()` usage patterns — both as a generation guide (apply these patterns when writing new code) and a review guide (flag missing priming in existing code).
+
+## Options Cache Priming — [options-cache-priming.md](options-cache-priming.md)
+
+Use when writing or reviewing code that reads multiple non-autoloaded options in a loop or method. Covers `wp_prime_option_caches()` usage patterns for static key lists, derivable key patterns, and dynamically extracted key sets (excluding autoloaded options, which are already in cache).
diff --git a/.ai/skills/woocommerce-performance/options-cache-priming.md b/.ai/skills/woocommerce-performance/options-cache-priming.md
new file mode 100644
index 00000000000..70b3d69593a
--- /dev/null
+++ b/.ai/skills/woocommerce-performance/options-cache-priming.md
@@ -0,0 +1,136 @@
+# Options Cache Priming
+
+Covers correct usage of `wp_prime_option_caches()` to reduce SQL query counts when reading multiple options in a method or loop.
+
+## Patterns
+
+### 1. Missing options priming before reading a known set of keys
+
+**Apply when:** A method reads multiple known `get_option()` keys in sequence.
+
+**Correct pattern:**
+
+```php
+// Prime caches to reduce future queries.
+wp_prime_option_caches(
+ array(
+ 'woocommerce_enable_checkout_login_reminder',
+ 'woocommerce_tax_display_cart',
+ // ...
+ )
+);
+$login_reminder = get_option( 'woocommerce_enable_checkout_login_reminder' );
+$tax_display = get_option( 'woocommerce_tax_display_cart' );
+```
+
+No `! empty()` guard is needed for statically declared, always-non-empty arrays. Place the comment directly above the call.
+
+**Common locations to check:**
+
+- `register_routes()` methods that read options immediately after registration
+- Block type `render()` or `get_data()` methods that read several settings
+- Any method that reads more than one non-autoloaded option in sequence
+
+---
+
+### 2. Missing options priming before a loop with a derivable key pattern
+
+**Apply when:** A loop iterates a collection and each iteration calls `get_option()` using a key derived from the item — for example `woocommerce_{class}_settings`.
+
+**Correct pattern:**
+
+```php
+// Prime caches to reduce future queries.
+wp_prime_option_caches(
+ array_map( fn( string $class ) => sprintf( 'woocommerce_%s_settings', $class ), $classes )
+);
+foreach ( $classes as $class ) {
+ $settings = get_option( sprintf( 'woocommerce_%s_settings', $class ) );
+}
+```
+
+**Common locations to check:**
+
+- Email class initialization: key pattern `woocommerce_{email_class_suffix}_settings`
+- Shipping method loops: key pattern `woocommerce_{method}_settings`
+
+---
+
+### 3. Missing options priming when keys are extracted from a settings structure
+
+**Apply when:** A settings array carries an `option_key` field; the array is iterated and each item's option is read via `get_option()`.
+
+**Correct pattern:**
+
+```php
+$prefetch = array_column( $settings, 'option_key' ); // or equivalent extraction
+if ( ! empty( $prefetch ) ) {
+ // Prime caches to reduce future queries.
+ wp_prime_option_caches( $prefetch );
+}
+foreach ( $settings as $setting ) {
+ $value = get_option( $setting['option_key'] );
+}
+```
+
+Guard with `! empty()` when the list is dynamically built and may be empty. When guarded, the comment sits inside the `if` block directly above the call — consistent with `_prime_post_caches` placement rules.
+
+---
+
+## Notes
+
+`wp_prime_option_caches()` is a stable public WordPress function (no underscore prefix), available since WP 6.4. WooCommerce's minimum supported WordPress version guarantees its presence — no `is_callable()` guard is needed.
+
+Always use the comment `// Prime caches to reduce future queries.` directly above the call. When the call is guarded by `! empty()`, the comment sits inside the `if` block — not before it.
+
+The benefit of `wp_prime_option_caches` operates along two complementary dimensions — not binary logic:
+
+- **Existence**: options not yet written to the database are absent from `wp_load_alloptions()` even when flagged autoloaded. Each `get_option()` call for a missing key issues an individual SQL query. Priming batches those misses into one query upfront.
+- **Autoload state**: non-autoloaded options are never loaded at bootstrap regardless of whether they exist. Priming is the primary mechanism to avoid per-request queries for them.
+
+An autoloaded option that has already been saved gains nothing from priming (already in cache). The same option before it is first saved benefits from the existence check. Both dimensions apply independently — consider both when deciding whether to prime.
+
+For multisite contexts, use `wp_prime_network_option_caches( $network_id, $keys )` (available since WP 6.4) for network-scoped options.
+
+---
+
+## Autoload Architecture (WooCommerce-specific)
+
+**WooCommerce settings API autoloads by default.** Any option registered and saved through `WC_Admin_Settings::save_fields()` is stored with `autoload = 'yes'` unless the field definition explicitly sets `'autoload' => false`. The relevant code is in `includes/admin/class-wc-admin-settings.php`:
+
+```php
+// Line ~1035
+$autoload_options[ $option_name ] = isset( $option['autoload'] ) ? (bool) $option['autoload'] : true;
+// Line ~1047
+update_option( $name, $value, $autoload_options[ $name ] ? 'yes' : 'no' );
+```
+
+WordPress loads all autoloaded options into the object cache at bootstrap via `wp_load_alloptions()`. This means that **any `get_option()` call reading a WooCommerce settings-API-registered option is already served from cache** — adding `wp_prime_option_caches` there is a no-op.
+
+### False-positive patterns — do NOT add priming
+
+High `get_option()` concentration alone is **not** a signal. These are common false positives:
+
+- **Endpoint options** — `woocommerce_checkout_pay_endpoint`, `woocommerce_myaccount_*_endpoint`, etc. All autoloaded via settings API.
+- **Feature flags and toggles** — `woocommerce_enable_ajax_add_to_cart`, `woocommerce_enable_checkout_login_reminder`, `woocommerce_tax_display_cart`, etc. All autoloaded.
+- **General store settings** — currency, weight unit, address fields, etc. All autoloaded.
+
+### The `*_settings` per-entity pattern
+
+All three entity types extend `WC_Settings_API`, which saves settings with `autoload='yes'`. Once saved, these options are already in cache. However, on a fresh install or before settings are first saved, they are absent from `wp_load_alloptions()` — each `get_option()` issues an individual query. Priming is justified here specifically for the existence dimension (batching those misses), particularly when looping over a large number of entities such as email classes.
+
+The four built-in payment gateways are a negligible count and are skipped.
+
+| Location | Pattern | Status |
+| --- | --- | --- |
+| `includes/class-wc-emails.php` — `init()` | array_map over email class list | ✅ covered — batches miss queries on fresh/unconfigured installs |
+| `includes/class-wc-shipping.php` — `get_shipping_method_class_names()` | array_map over method ID list | ✅ covered — same rationale |
+| `includes/class-wc-payment-gateways.php` — `init()` | 4 built-in gateways — negligible count | ✅ verified, skipped |
+
+### Workflow for gap analysis
+
+When asked to find missing `wp_prime_option_caches` opportunities:
+
+1. Search for multi-`get_option()` methods.
+2. Consider both dimensions: autoload state (non-autoloaded options benefit on every request) and existence (options not yet saved benefit on first use regardless of autoload flag).
+3. Flag loops or sequences reading multiple options where either dimension applies and no priming is present.
diff --git a/plugins/woocommerce/changelog/performance-extract-options-cache-priming-prs-into-skill b/plugins/woocommerce/changelog/performance-extract-options-cache-priming-prs-into-skill
new file mode 100644
index 00000000000..00285f15495
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-extract-options-cache-priming-prs-into-skill
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Introduce a new performance skill focused on effectively using option cache priming APIs.
diff --git a/plugins/woocommerce/includes/class-wc-emails.php b/plugins/woocommerce/includes/class-wc-emails.php
index 7b1c1548f43..75dafef0813 100644
--- a/plugins/woocommerce/includes/class-wc-emails.php
+++ b/plugins/woocommerce/includes/class-wc-emails.php
@@ -308,7 +308,7 @@ class WC_Emails {
$emails['WC_Email_Customer_Fulfillment_Deleted'] = __DIR__ . '/emails/class-wc-email-customer-fulfillment-deleted.php';
}
- // Preload the options which will be used when emails are getting initialized in the loop below (reduces the number of SQL-queries).
+ // Prime caches to reduce future queries.
wp_prime_option_caches(
array_map(
fn( string $class_name ) => sprintf( 'woocommerce_%s_settings', strtolower( str_replace( 'WC_Email_', '', $class_name ) ) ),
diff --git a/plugins/woocommerce/includes/class-wc-payment-gateways.php b/plugins/woocommerce/includes/class-wc-payment-gateways.php
index 444e97c4435..dede9b12a45 100644
--- a/plugins/woocommerce/includes/class-wc-payment-gateways.php
+++ b/plugins/woocommerce/includes/class-wc-payment-gateways.php
@@ -91,6 +91,8 @@ class WC_Payment_Gateways {
// Filter.
$load_gateways = apply_filters( 'woocommerce_payment_gateways', $load_gateways );
+ // No wp_prime_option_caches needed: gateway settings are autoloaded (WC_Settings_API saves with autoload='yes').
+
// Get sort order option.
$ordering = (array) get_option( 'woocommerce_gateway_order' );
$order_end = 999;
diff --git a/plugins/woocommerce/includes/class-wc-shipping.php b/plugins/woocommerce/includes/class-wc-shipping.php
index 62b28419ffd..0859d0cde6d 100644
--- a/plugins/woocommerce/includes/class-wc-shipping.php
+++ b/plugins/woocommerce/includes/class-wc-shipping.php
@@ -139,7 +139,7 @@ class WC_Shipping {
// For backwards compatibility with 2.5.x we load any ENABLED legacy shipping methods here.
$maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' );
- // Prime the settings options: reduces the number of executed SQLs.
+ // Prime caches to reduce future queries.
wp_prime_option_caches( array_map( fn( string $method ) => sprintf( 'woocommerce_%s_settings', $method ), $maybe_load_legacy_methods ) );
foreach ( $maybe_load_legacy_methods as $method ) {
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php
index 5bc451ef2c2..8f16b8c5308 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php
@@ -42,7 +42,7 @@ class WC_REST_Customers_V1_Controller extends WC_REST_Controller {
* Register the routes for customers.
*/
public function register_routes() {
- // Preload the options which will be used in this method (reduces the number of SQL-queries).
+ // Prime caches to reduce future queries.
wp_prime_option_caches(
array(
'woocommerce_registration_generate_username',
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php
index a3cdc6f421e..0b51d9a8349 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php
@@ -228,6 +228,7 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller {
}
}
if ( array() !== $prefetch ) {
+ // Prime caches to reduce future queries.
wp_prime_option_caches( $prefetch );
}
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
index c7ed5870b08..320d56a5013 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
@@ -443,7 +443,7 @@ class Checkout extends AbstractBlock {
FILTER_VALIDATE_BOOLEAN
)
);
- // Optimization note: reduce the number of SQLs required to fetch the options in the lines below.
+ // Prime caches to reduce future queries.
wp_prime_option_caches(
array(
'woocommerce_enable_checkout_login_reminder',
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings.php b/plugins/woocommerce/src/Internal/Admin/Settings.php
index 26ffa1e42b5..f7dff32332e 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings.php
@@ -154,6 +154,7 @@ class Settings {
//phpcs:ignore
$preload_options = apply_filters( 'woocommerce_admin_preload_options', array() );
if ( ! empty( $preload_options ) ) {
+ // Prime caches to reduce future queries.
wp_prime_option_caches( $preload_options );
foreach ( $preload_options as $option ) {
$settings['preloadOptions'][ $option ] = get_option( $option );