Install
$ agentstack add skill-iwritec0de-wp-dev-wordpress-performance ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
WordPress Performance
Performance optimization patterns for WordPress plugins, themes, and live sites.
Critical Rules
- Never query without limits —
posts_per_page => -1and unbounded$wpdbqueries are the most common cause of memory exhaustion on production sites. - Cache expensive queries — any query that runs on every page load and doesn't change per-request must be cached with transients or the object cache.
- Use
no_found_rows— set'no_found_rows' => trueon anyWP_Querythat doesn't need pagination; this skipsSQL_CALC_FOUND_ROWSwhich is expensive on large tables. - Avoid meta queries on unindexed keys —
meta_queryonwp_postmetaperforms full table scans unless you add custom indexes. Consider a custom table for heavily queried data. - Defer heavy work — operations on
initrun on every request. Useadmin_initfor admin-only work,rest_api_initfor REST-only, andwp_loadedor later hooks when possible. - Prime caches, don't loop-query — use
update_post_meta_cache(),update_post_caches(), or_prime_post_caches()to batch-load metadata instead of callingget_post_meta()in a loop.
Query Performance Patterns
Efficient WP_Query
// Good — limited, cache-friendly, no unnecessary data:
$query = new WP_Query( [
'post_type' => 'product',
'posts_per_page' => 20,
'no_found_rows' => true, // Skip pagination count query.
'update_post_meta_cache' => false, // Skip if not reading meta.
'update_post_term_cache' => false, // Skip if not reading terms.
'fields' => 'ids', // Return only IDs when full objects aren't needed.
] );
// Bad — unbounded, forces full table scan:
$query = new WP_Query( [
'post_type' => 'product',
'posts_per_page' => -1, // Never do this.
] );
N+1 Query Prevention
// Bad — N+1 pattern (1 query per post in the loop):
foreach ( $posts as $post ) {
$price = get_post_meta( $post->ID, '_price', true ); // Query per iteration.
}
// Good — prime the cache first, then loop reads from cache:
$post_ids = wp_list_pluck( $posts, 'ID' );
update_meta_cache( 'post', $post_ids ); // Single query loads all meta.
foreach ( $posts as $post ) {
$price = get_post_meta( $post->ID, '_price', true ); // Reads from cache.
}
Batch Operations
// Bad — individual inserts in a loop:
foreach ( $items as $item ) {
$wpdb->insert( $table, $item ); // N queries.
}
// Good — single bulk insert:
$values = [];
foreach ( $items as $item ) {
$values[] = $wpdb->prepare( '(%d, %s, %f)', $item['user_id'], $item['status'], $item['total'] );
}
if ( $values ) {
$wpdb->query( "INSERT INTO {$table} (user_id, status, total) VALUES " . implode( ', ', $values ) );
}
Caching Strategies
Transient Caching Pattern
function myplugin_get_featured_products(): array {
$cache_key = 'myplugin_featured_products';
$data = get_transient( $cache_key );
if ( false !== $data ) {
return $data;
}
$query = new WP_Query( [
'post_type' => 'product',
'posts_per_page' => 12,
'meta_key' => '_featured',
'meta_value' => 'yes',
'no_found_rows' => true,
'fields' => 'ids',
] );
$data = $query->posts;
set_transient( $cache_key, $data, HOUR_IN_SECONDS );
return $data;
}
// Invalidate when products change:
add_action( 'save_post_product', function (): void {
delete_transient( 'myplugin_featured_products' );
} );
Object Cache for Per-Request Deduplication
function myplugin_get_settings(): array {
$cached = wp_cache_get( 'settings', 'myplugin' );
if ( false !== $cached ) {
return $cached;
}
$settings = get_option( 'myplugin_settings', [] );
wp_cache_set( 'settings', $settings, 'myplugin' );
return $settings;
}
When to Use Each Layer
| Scenario | Strategy | |----------|----------| | Same data fetched multiple times per request | wp_cache_* (object cache) | | Expensive query, result valid for minutes/hours | set_transient() | | External API response | set_transient() with timeout matching API rate limits | | Data that changes on save | Transient + delete_transient() on save_post | | User-specific data | Transient with user ID in key, or get_user_meta() | | Full-page output | Consider wp_cache_* with a persistent backend (Redis/Memcached) |
Hook Performance
Defer Initialization
// Bad — runs on every request including REST, AJAX, cron:
add_action( 'init', 'myplugin_heavy_init' );
// Good — scope to context:
add_action( 'admin_init', 'myplugin_admin_setup' ); // Admin only.
add_action( 'rest_api_init', 'myplugin_register_routes' ); // REST only.
add_action( 'template_redirect', 'myplugin_frontend' ); // Frontend only.
// Good — conditional loading:
add_action( 'init', function (): void {
if ( ! is_admin() && ! wp_doing_ajax() && ! wp_doing_cron() ) {
// Frontend-only logic.
}
} );
Lazy Loading
// Bad — always loads all classes:
require_once __DIR__ . '/includes/class-admin.php';
require_once __DIR__ . '/includes/class-reports.php';
require_once __DIR__ . '/includes/class-import.php';
// Good — load only when needed:
add_action( 'admin_menu', function (): void {
require_once __DIR__ . '/includes/class-admin.php';
new Myplugin_Admin();
} );
// Best — PSR-4 autoloader via Composer:
require_once __DIR__ . '/vendor/autoload.php';
Autoload Optimization
Options with autoload = yes are loaded into memory on every page load via a single SELECT from wp_options. Large or rarely-used options bloat this query.
// Bad — large data autoloaded:
add_option( 'myplugin_log_history', $huge_array ); // Defaults to autoload = yes.
// Good — disable autoload for large or infrequent data:
add_option( 'myplugin_log_history', $huge_array, '', false );
// Or with update_option (autoload param is 4th arg since WP 4.2):
update_option( 'myplugin_log_history', $huge_array, false );
Rule of thumb: autoload only small, frequently-accessed options (settings, feature flags). Disable autoload for logs, large serialized arrays, and data accessed only on specific pages.
Database Indexing
// Add indexes to custom tables on activation:
register_activation_hook( __FILE__, function (): void {
global $wpdb;
$table = $wpdb->prefix . 'myplugin_orders';
// Check before adding to make activation idempotent.
$index_exists = $wpdb->get_var( $wpdb->prepare(
'SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND INDEX_NAME = %s',
DB_NAME, $table, 'status_created_idx'
) );
if ( ! $index_exists ) {
$wpdb->query( "ALTER TABLE {$table} ADD INDEX status_created_idx (status, created_at)" );
}
} );
Indexing Post Meta for Queries
If you frequently filter by a specific meta key, consider adding an index:
-- Only add this if meta_query on this key is a known bottleneck:
ALTER TABLE wp_postmeta ADD INDEX meta_key_value_idx (meta_key(50), meta_value(50));
Warning: modifying core tables is risky and may conflict with updates. Prefer custom tables for heavily queried structured data.
Anti-Patterns
| Anti-Pattern | Impact | Fix | |---|---|---| | posts_per_page => -1 | Loads unlimited posts into memory | Set a reasonable limit or paginate | | get_post_meta() in a loop | N+1 queries | update_meta_cache() before loop | | Heavy logic on init | Runs on every request | Use context-specific hooks | | get_option() on every function call | Redundant queries (without persistent cache) | Cache in a static variable or wp_cache_* | | Large serialized arrays in autoloaded options | Bloats the autoload query | Set autoload to false | | $wpdb->query() inside foreach | N inserts instead of 1 | Batch insert with concatenated VALUES | | LIKE '%search%' on large tables | Cannot use index (leading wildcard) | Full-text search index or prefix-only LIKE | | Missing no_found_rows | Extra COUNT query when pagination isn't needed | Set true on non-paginated queries | | wp_remote_get() on every page load | Blocks rendering on external service | Cache response with transient | | Autoloading large options | Loaded on every request even when unused | add_option() with autoload false |
For query analysis and EXPLAIN usage, see reference/query-optimization.md. For caching architecture and invalidation strategies, see reference/caching-strategies.md.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: iwritec0de
- Source: iwritec0de/wp-dev
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.