Initial commit
This commit is contained in:
15
.claude-plugin/plugin.json
Normal file
15
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "claude-wordpress-skills",
|
||||||
|
"description": "Professional WordPress engineering skills for Claude Code — performance optimization, security auditing, Gutenberg block development, and theme/plugin best practices",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Elvis Morales",
|
||||||
|
"url": "https://github.com/elvismdev"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# claude-wordpress-skills
|
||||||
|
|
||||||
|
Professional WordPress engineering skills for Claude Code — performance optimization, security auditing, Gutenberg block development, and theme/plugin best practices
|
||||||
10
commands/wp-perf-review.md
Normal file
10
commands/wp-perf-review.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
description: WordPress performance code review - detects database anti-patterns, caching issues, hook problems, and scalability concerns
|
||||||
|
argument-hint: [file-or-directory]
|
||||||
|
---
|
||||||
|
|
||||||
|
Use and follow the **wp-performance-review** skill to perform a comprehensive WordPress performance code review.
|
||||||
|
|
||||||
|
**Target**: $ARGUMENTS (if empty, use current working directory)
|
||||||
|
|
||||||
|
Execute the full Code Review Workflow from the skill, load reference files as needed for deeper analysis, and format output using the skill's Output Format section with severity levels (Critical/Warning/Info).
|
||||||
12
commands/wp-perf.md
Normal file
12
commands/wp-perf.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
description: Quick WordPress performance scan - fast triage using critical pattern detection
|
||||||
|
argument-hint: [path]
|
||||||
|
---
|
||||||
|
|
||||||
|
Use the **wp-performance-review** skill to perform a quick triage scan.
|
||||||
|
|
||||||
|
**Target**: $ARGUMENTS (if empty, use current working directory)
|
||||||
|
|
||||||
|
Focus only on the "Search Patterns for Quick Detection" section—run the grep commands to find critical issues fast. Report matches with file:line references and severity levels. Skip deep analysis.
|
||||||
|
|
||||||
|
If critical issues are found, suggest running `/wp-perf-review` for comprehensive analysis with fixes.
|
||||||
69
plugin.lock.json
Normal file
69
plugin.lock.json
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:elvismdev/claude-wordpress-skills:",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "dbedf97474919869900c1e9b230f5c5cfde060d3",
|
||||||
|
"treeHash": "7ad5a5dedea7a1a6bfa8921ac7cce75bc2f15b623ab1286d9df2d2f6a20a22b7",
|
||||||
|
"generatedAt": "2025-11-28T10:16:46.558593Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "claude-wordpress-skills",
|
||||||
|
"description": "Professional WordPress engineering skills for Claude Code — performance optimization, security auditing, Gutenberg block development, and theme/plugin best practices",
|
||||||
|
"version": "1.3.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "8a847370c2883acd2420a9ab999255e50e8c361abcf326063019829efab274ee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "37d365a1063c96b47510a10750d1002535b7882121614cc32a4fa69241992036"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/wp-perf.md",
|
||||||
|
"sha256": "6cc0947f472dc34d3271bc92d0398dda20a4fc8baa8ce97531d00d8a6af236d2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/wp-perf-review.md",
|
||||||
|
"sha256": "fc4794fb42009cfa741b651c287923694bacbccad6de55e9be8f7c267bd8c77f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wp-performance-review/SKILL.md",
|
||||||
|
"sha256": "b6f975daa6d21abcc8a780c32f60e225979fbcd91a614c8588080a532fa8a616"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wp-performance-review/references/wp-query-guide.md",
|
||||||
|
"sha256": "4c293902a19c43527d9067cc6edcc8a8fc0c52a4a649c1990a67e5aa6799faca"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wp-performance-review/references/caching-guide.md",
|
||||||
|
"sha256": "fac5821ae4fcace1286473b4b500ab094ce35377ba66a50783794589e93ba297"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wp-performance-review/references/anti-patterns.md",
|
||||||
|
"sha256": "c1ca2b34eeda0540cdf638be326e44e9d8324f804e52f8ddff0b6b54294f8478"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wp-performance-review/references/measurement-guide.md",
|
||||||
|
"sha256": "c6973ac29340563ceeffb3f1bd79547898959577f462ded0c368e6c91e237e24"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "7ad5a5dedea7a1a6bfa8921ac7cce75bc2f15b623ab1286d9df2d2f6a20a22b7"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
506
skills/wp-performance-review/SKILL.md
Normal file
506
skills/wp-performance-review/SKILL.md
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
---
|
||||||
|
name: wp-performance-review
|
||||||
|
description: Use when reviewing WordPress PHP code for performance issues, auditing themes/plugins for scalability, optimizing WP_Query, analyzing caching strategies, or when user mentions "slow WordPress", "high-traffic", "performance review", "timeout", "500 error", "out of memory", or "site won't load". Detects anti-patterns in database queries, hooks, object caching, AJAX, and template loading that cause failures at scale.
|
||||||
|
---
|
||||||
|
|
||||||
|
# WordPress Performance Review Skill
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Systematic performance code review for WordPress themes, plugins, and custom code. **Core principle:** Scan critical issues first (OOM, unbounded queries, cache bypass), then warnings, then optimizations. Report with line numbers and severity levels.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
**Use when:**
|
||||||
|
- Reviewing PR/code for WordPress theme or plugin
|
||||||
|
- User reports slow page loads, timeouts, or 500 errors
|
||||||
|
- Auditing before high-traffic event (launch, sale, viral moment)
|
||||||
|
- Optimizing WP_Query or database operations
|
||||||
|
- Investigating memory exhaustion or DB locks
|
||||||
|
|
||||||
|
**Don't use for:**
|
||||||
|
- Security-only audits (use wp-security-review when available)
|
||||||
|
- Gutenberg block development patterns (use wp-gutenberg-blocks when available)
|
||||||
|
- General PHP code review not specific to WordPress
|
||||||
|
|
||||||
|
## Code Review Workflow
|
||||||
|
|
||||||
|
1. **Identify file type** and apply relevant checks below
|
||||||
|
2. **Scan for critical patterns first** (OOM, unbounded queries, cache bypass)
|
||||||
|
3. **Check warnings** (inefficient but not catastrophic)
|
||||||
|
4. **Note optimizations** (nice-to-have improvements)
|
||||||
|
5. **Report with line numbers** using output format below
|
||||||
|
|
||||||
|
## File-Type Specific Checks
|
||||||
|
|
||||||
|
### Plugin/Theme PHP Files (`functions.php`, `plugin.php`, `*.php`)
|
||||||
|
Scan for:
|
||||||
|
- `query_posts()` → CRITICAL: Never use - breaks main query
|
||||||
|
- `posts_per_page.*-1` or `numberposts.*-1` → CRITICAL: Unbounded query
|
||||||
|
- `session_start()` → CRITICAL: Bypasses page cache
|
||||||
|
- `add_action.*init.*` or `add_action.*wp_loaded` → Check if expensive code runs every request
|
||||||
|
- `update_option` or `add_option` in non-admin context → WARNING: DB writes on page load
|
||||||
|
- `wp_remote_get` or `wp_remote_post` without caching → WARNING: Blocking HTTP
|
||||||
|
|
||||||
|
### WP_Query / Database Code
|
||||||
|
Scan for:
|
||||||
|
- Missing `posts_per_page` argument → WARNING: Defaults to blog setting
|
||||||
|
- `'meta_query'` with `'value'` comparisons → WARNING: Unindexed column scan
|
||||||
|
- `post__not_in` with large arrays → WARNING: Slow exclusion
|
||||||
|
- `LIKE '%term%'` (leading wildcard) → WARNING: Full table scan
|
||||||
|
- Missing `no_found_rows => true` when not paginating → INFO: Unnecessary count
|
||||||
|
|
||||||
|
### AJAX Handlers (`wp_ajax_*`, REST endpoints)
|
||||||
|
Scan for:
|
||||||
|
- `admin-ajax.php` usage → INFO: Consider REST API instead
|
||||||
|
- POST method for read operations → WARNING: Bypasses cache
|
||||||
|
- `setInterval` or polling patterns → CRITICAL: Self-DDoS risk
|
||||||
|
- Missing nonce verification → Security issue (not performance, but flag it)
|
||||||
|
|
||||||
|
### Template Files (`*.php` in theme)
|
||||||
|
Scan for:
|
||||||
|
- `get_template_part` in loops → WARNING: Consider caching output
|
||||||
|
- Database queries inside loops (N+1) → CRITICAL: Query multiplication
|
||||||
|
- `wp_remote_get` in templates → WARNING: Blocks rendering
|
||||||
|
|
||||||
|
### JavaScript Files
|
||||||
|
Scan for:
|
||||||
|
- `$.post(` for read operations → WARNING: Use GET for cacheability
|
||||||
|
- `setInterval.*fetch\|ajax` → CRITICAL: Polling pattern
|
||||||
|
- `import _ from 'lodash'` → WARNING: Full library import bloats bundle
|
||||||
|
- Inline `<script>` making AJAX calls on load → Check necessity
|
||||||
|
|
||||||
|
### Block Editor / Gutenberg Files (`block.json`, `*.js` in blocks/)
|
||||||
|
Scan for:
|
||||||
|
- Many `registerBlockStyle()` calls → WARNING: Each creates preview iframe
|
||||||
|
- `wp_kses_post($content)` in render callbacks → WARNING: Breaks InnerBlocks
|
||||||
|
- Static blocks without `render_callback` → INFO: Consider dynamic for maintainability
|
||||||
|
|
||||||
|
### Asset Registration (`functions.php`, `*.php`)
|
||||||
|
Scan for:
|
||||||
|
- `wp_enqueue_script` without version → INFO: Cache busting issues
|
||||||
|
- `wp_enqueue_script` without `defer`/`async` strategy → INFO: Blocks rendering
|
||||||
|
- Missing `THEME_VERSION` constant → INFO: Version management
|
||||||
|
- `wp_enqueue_script` without conditional check → WARNING: Assets load globally when only needed on specific pages
|
||||||
|
|
||||||
|
### Transients & Options
|
||||||
|
Scan for:
|
||||||
|
- `set_transient` with dynamic keys (e.g., `user_{$id}`) → WARNING: Table bloat without object cache
|
||||||
|
- `set_transient` for frequently-changing data → WARNING: Defeats caching purpose
|
||||||
|
- Large data in transients on shared hosting → WARNING: DB bloat without object cache
|
||||||
|
|
||||||
|
### WP-Cron
|
||||||
|
Scan for:
|
||||||
|
- Missing `DISABLE_WP_CRON` constant → INFO: Cron runs on page requests
|
||||||
|
- Long-running cron callbacks (loops over all users/posts) → CRITICAL: Blocks cron queue
|
||||||
|
- `wp_schedule_event` without checking if already scheduled → WARNING: Duplicate schedules
|
||||||
|
|
||||||
|
## Search Patterns for Quick Detection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Critical issues - scan these first
|
||||||
|
grep -rn "posts_per_page.*-1\|numberposts.*-1" .
|
||||||
|
grep -rn "query_posts\s*(" .
|
||||||
|
grep -rn "session_start\s*(" .
|
||||||
|
grep -rn "setInterval.*fetch\|setInterval.*ajax\|setInterval.*\\\$\." .
|
||||||
|
|
||||||
|
# Database writes on frontend
|
||||||
|
grep -rn "update_option\|add_option" . | grep -v "admin\|activate\|install"
|
||||||
|
|
||||||
|
# Uncached expensive functions
|
||||||
|
grep -rn "url_to_postid\|attachment_url_to_postid\|count_user_posts" .
|
||||||
|
|
||||||
|
# External HTTP without caching
|
||||||
|
grep -rn "wp_remote_get\|wp_remote_post\|file_get_contents.*http" .
|
||||||
|
|
||||||
|
# Cache bypass risks
|
||||||
|
grep -rn "setcookie\|session_start" .
|
||||||
|
|
||||||
|
# PHP code anti-patterns
|
||||||
|
grep -rn "in_array\s*(" . | grep -v "true\s*)" # Missing strict comparison
|
||||||
|
grep -rn "<<<" . # Heredoc/nowdoc syntax
|
||||||
|
grep -rn "cache_results.*false" .
|
||||||
|
|
||||||
|
# JavaScript bundle issues
|
||||||
|
grep -rn "import.*from.*lodash['\"]" . # Full lodash import
|
||||||
|
grep -rn "registerBlockStyle" . # Many block styles = performance issue
|
||||||
|
|
||||||
|
# Asset loading issues
|
||||||
|
grep -rn "wp_enqueue_script\|wp_enqueue_style" . | grep -v "is_page\|is_singular\|is_admin"
|
||||||
|
|
||||||
|
# Transient misuse
|
||||||
|
grep -rn "set_transient.*\\\$" . # Dynamic transient keys
|
||||||
|
grep -rn "set_transient" . | grep -v "get_transient" # Set without checking first
|
||||||
|
|
||||||
|
# WP-Cron issues
|
||||||
|
grep -rn "wp_schedule_event" . | grep -v "wp_next_scheduled" # Missing schedule check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform Context
|
||||||
|
|
||||||
|
Different hosting environments require different approaches:
|
||||||
|
|
||||||
|
**Managed WordPress Hosts** (WP Engine, Pantheon, Pressable, WordPress VIP, etc.):
|
||||||
|
- Often provide object caching out of the box
|
||||||
|
- May have platform-specific helper functions (e.g., `wpcom_vip_*` on VIP)
|
||||||
|
- Check host documentation for recommended patterns
|
||||||
|
|
||||||
|
**Self-Hosted / Standard Hosting**:
|
||||||
|
- Implement object caching wrappers manually for expensive functions
|
||||||
|
- Consider Redis or Memcached plugins for persistent object cache
|
||||||
|
- More responsibility for caching layer configuration
|
||||||
|
|
||||||
|
**Shared Hosting**:
|
||||||
|
- Be extra cautious about unbounded queries and external HTTP
|
||||||
|
- Limited resources mean performance issues surface faster
|
||||||
|
- May lack persistent object cache entirely
|
||||||
|
|
||||||
|
## Quick Reference: Critical Anti-Patterns
|
||||||
|
|
||||||
|
### Database Queries
|
||||||
|
```php
|
||||||
|
// ❌ CRITICAL: Unbounded query.
|
||||||
|
'posts_per_page' => -1
|
||||||
|
|
||||||
|
// ✅ GOOD: Set reasonable limit, paginate if needed.
|
||||||
|
'posts_per_page' => 100,
|
||||||
|
'no_found_rows' => true, // Skip count if not paginating.
|
||||||
|
|
||||||
|
// ❌ CRITICAL: Never use query_posts().
|
||||||
|
query_posts( 'cat=1' ); // Breaks pagination, conditionals.
|
||||||
|
|
||||||
|
// ✅ GOOD: Use WP_Query or pre_get_posts filter.
|
||||||
|
$query = new WP_Query( array( 'cat' => 1 ) );
|
||||||
|
// Or modify main query:
|
||||||
|
add_action( 'pre_get_posts', function( $query ) {
|
||||||
|
if ( $query->is_main_query() && ! is_admin() ) {
|
||||||
|
$query->set( 'cat', 1 );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
// ❌ CRITICAL: Missing WHERE clause (falsy ID becomes 0).
|
||||||
|
$query = new WP_Query( array( 'p' => intval( $maybe_false_id ) ) );
|
||||||
|
|
||||||
|
// ✅ GOOD: Validate ID before querying.
|
||||||
|
if ( ! empty( $maybe_false_id ) ) {
|
||||||
|
$query = new WP_Query( array( 'p' => intval( $maybe_false_id ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ WARNING: LIKE with leading wildcard (full table scan).
|
||||||
|
$wpdb->get_results( "SELECT * FROM wp_posts WHERE post_title LIKE '%term%'" );
|
||||||
|
|
||||||
|
// ✅ GOOD: Use trailing wildcard only, or use WP_Query 's' parameter.
|
||||||
|
$wpdb->get_results( $wpdb->prepare(
|
||||||
|
"SELECT * FROM wp_posts WHERE post_title LIKE %s",
|
||||||
|
$wpdb->esc_like( $term ) . '%'
|
||||||
|
) );
|
||||||
|
|
||||||
|
// ❌ WARNING: NOT IN queries (filter in PHP instead).
|
||||||
|
'post__not_in' => $excluded_ids
|
||||||
|
|
||||||
|
// ✅ GOOD: Fetch all, filter in PHP (faster for large exclusion lists).
|
||||||
|
$posts = get_posts( array( 'posts_per_page' => 100 ) );
|
||||||
|
$posts = array_filter( $posts, function( $post ) use ( $excluded_ids ) {
|
||||||
|
return ! in_array( $post->ID, $excluded_ids, true );
|
||||||
|
} );
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hooks & Actions
|
||||||
|
```php
|
||||||
|
// ❌ WARNING: Code runs on every request via init.
|
||||||
|
add_action( 'init', 'expensive_function' );
|
||||||
|
|
||||||
|
// ✅ GOOD: Check context before running expensive code.
|
||||||
|
add_action( 'init', function() {
|
||||||
|
if ( is_admin() || wp_doing_cron() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Frontend-only code here.
|
||||||
|
} );
|
||||||
|
|
||||||
|
// ❌ CRITICAL: Database writes on every page load.
|
||||||
|
add_action( 'wp_head', 'prefix_bad_tracking' );
|
||||||
|
function prefix_bad_tracking() {
|
||||||
|
update_option( 'last_visit', time() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Use object cache buffer, flush via cron.
|
||||||
|
add_action( 'shutdown', function() {
|
||||||
|
wp_cache_incr( 'page_views_buffer', 1, 'counters' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// ❌ WARNING: Using admin-ajax.php instead of REST API.
|
||||||
|
// Prefer: register_rest_route() - leaner bootstrap.
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP Code
|
||||||
|
```php
|
||||||
|
// ❌ WARNING: O(n) lookup - use isset() with associative array.
|
||||||
|
in_array( $value, $array ); // Also missing strict = true.
|
||||||
|
|
||||||
|
// ✅ GOOD: O(1) lookup with isset().
|
||||||
|
$allowed = array( 'foo' => true, 'bar' => true );
|
||||||
|
if ( isset( $allowed[ $value ] ) ) {
|
||||||
|
// Process.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ WARNING: Heredoc prevents late escaping.
|
||||||
|
$html = <<<HTML
|
||||||
|
<div>$unescaped_content</div>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
// ✅ GOOD: Escape at output.
|
||||||
|
printf( '<div>%s</div>', esc_html( $content ) );
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching Issues
|
||||||
|
```php
|
||||||
|
// ❌ WARNING: Uncached expensive function calls.
|
||||||
|
url_to_postid( $url );
|
||||||
|
attachment_url_to_postid( $attachment_url );
|
||||||
|
count_user_posts( $user_id );
|
||||||
|
wp_oembed_get( $url );
|
||||||
|
|
||||||
|
// ✅ GOOD: Wrap with object cache (works on any host).
|
||||||
|
function prefix_cached_url_to_postid( $url ) {
|
||||||
|
$cache_key = 'url_to_postid_' . md5( $url );
|
||||||
|
$post_id = wp_cache_get( $cache_key, 'url_lookups' );
|
||||||
|
|
||||||
|
if ( false === $post_id ) {
|
||||||
|
$post_id = url_to_postid( $url );
|
||||||
|
wp_cache_set( $cache_key, $post_id, 'url_lookups', HOUR_IN_SECONDS );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $post_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: On WordPress VIP, use platform helpers instead.
|
||||||
|
// wpcom_vip_url_to_postid(), wpcom_vip_attachment_url_to_postid(), etc.
|
||||||
|
|
||||||
|
// ❌ WARNING: Large autoloaded options.
|
||||||
|
add_option( 'prefix_large_data', $data ); // Add: , '', 'no' for autoload.
|
||||||
|
|
||||||
|
// ❌ INFO: Missing wp_cache_get_multiple for batch lookups.
|
||||||
|
foreach ( $ids as $id ) {
|
||||||
|
wp_cache_get( "key_{$id}" );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AJAX & External Requests
|
||||||
|
```javascript
|
||||||
|
// ❌ WARNING: AJAX POST request (bypasses cache).
|
||||||
|
$.post( ajaxurl, data ); // Prefer: $.get() for read operations.
|
||||||
|
|
||||||
|
// ❌ CRITICAL: Polling pattern (self-DDoS).
|
||||||
|
setInterval( () => fetch( '/wp-json/...' ), 5000 );
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ WARNING: Synchronous external HTTP in page load.
|
||||||
|
wp_remote_get( $url ); // Cache result or move to cron.
|
||||||
|
|
||||||
|
// ✅ GOOD: Set timeout and handle errors.
|
||||||
|
$response = wp_remote_get( $url, array( 'timeout' => 2 ) );
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return get_fallback_data();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WP Cron
|
||||||
|
```php
|
||||||
|
// ❌ WARNING: WP Cron runs on page requests.
|
||||||
|
// Add to wp-config.php:
|
||||||
|
define( 'DISABLE_WP_CRON', true );
|
||||||
|
// Run via server cron: * * * * * wp cron event run --due-now
|
||||||
|
|
||||||
|
// ❌ CRITICAL: Long-running cron blocks entire queue.
|
||||||
|
add_action( 'my_daily_sync', function() {
|
||||||
|
foreach ( get_users() as $user ) { // 50k users = hours.
|
||||||
|
sync_user_data( $user );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
// ✅ GOOD: Batch processing with rescheduling.
|
||||||
|
add_action( 'my_batch_sync', function() {
|
||||||
|
$offset = (int) get_option( 'sync_offset', 0 );
|
||||||
|
$users = get_users( array( 'number' => 100, 'offset' => $offset ) );
|
||||||
|
|
||||||
|
if ( empty( $users ) ) {
|
||||||
|
delete_option( 'sync_offset' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $users as $user ) {
|
||||||
|
sync_user_data( $user );
|
||||||
|
}
|
||||||
|
|
||||||
|
update_option( 'sync_offset', $offset + 100 );
|
||||||
|
wp_schedule_single_event( time() + 60, 'my_batch_sync' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// ❌ WARNING: Scheduling without checking if already scheduled.
|
||||||
|
wp_schedule_event( time(), 'hourly', 'my_task' ); // Creates duplicates!
|
||||||
|
|
||||||
|
// ✅ GOOD: Check before scheduling.
|
||||||
|
if ( ! wp_next_scheduled( 'my_task' ) ) {
|
||||||
|
wp_schedule_event( time(), 'hourly', 'my_task' );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Bypass Issues
|
||||||
|
```php
|
||||||
|
// ❌ CRITICAL: Plugin starts PHP session on frontend (bypasses ALL page cache).
|
||||||
|
session_start(); // Check plugins for this - entire site becomes uncacheable!
|
||||||
|
|
||||||
|
// ❌ WARNING: Unique query params create cache misses.
|
||||||
|
// https://example.com/?utm_source=fb&utm_campaign=123&fbclid=abc
|
||||||
|
// Each unique URL = separate cache entry = cache miss.
|
||||||
|
// Solution: Strip marketing params at CDN/edge level.
|
||||||
|
|
||||||
|
// ❌ WARNING: Setting cookies on public pages.
|
||||||
|
setcookie( 'visitor_id', $id ); // Prevents caching for that user.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transients Misuse
|
||||||
|
```php
|
||||||
|
// ❌ WARNING: Dynamic transient keys create table bloat (without object cache).
|
||||||
|
set_transient( "user_{$user_id}_cart", $data, HOUR_IN_SECONDS );
|
||||||
|
// 10,000 users = 10,000 rows in wp_options!
|
||||||
|
|
||||||
|
// ✅ GOOD: Use object cache for user-specific data.
|
||||||
|
wp_cache_set( "cart_{$user_id}", $data, 'user_carts', HOUR_IN_SECONDS );
|
||||||
|
|
||||||
|
// ❌ WARNING: Transients for frequently-changing data defeats purpose.
|
||||||
|
set_transient( 'visitor_count', $count, 60 ); // Changes every minute.
|
||||||
|
|
||||||
|
// ✅ GOOD: Use object cache for volatile data.
|
||||||
|
wp_cache_set( 'visitor_count', $count, 'stats' );
|
||||||
|
|
||||||
|
// ❌ WARNING: Large data in transients on shared hosting.
|
||||||
|
set_transient( 'api_response', $megabytes_of_json, DAY_IN_SECONDS );
|
||||||
|
// Without object cache = serialized blob in wp_options.
|
||||||
|
|
||||||
|
// ✅ GOOD: Check hosting before using transients for large data.
|
||||||
|
if ( wp_using_ext_object_cache() ) {
|
||||||
|
set_transient( 'api_response', $data, DAY_IN_SECONDS );
|
||||||
|
} else {
|
||||||
|
// Store in files or skip caching on shared hosting.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Asset Loading
|
||||||
|
```php
|
||||||
|
// ❌ WARNING: Assets load globally when only needed on specific pages.
|
||||||
|
add_action( 'wp_enqueue_scripts', function() {
|
||||||
|
wp_enqueue_script( 'contact-form-js', ... );
|
||||||
|
wp_enqueue_style( 'contact-form-css', ... );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// ✅ GOOD: Conditional enqueue based on page/template.
|
||||||
|
add_action( 'wp_enqueue_scripts', function() {
|
||||||
|
if ( is_page( 'contact' ) || is_page_template( 'contact-template.php' ) ) {
|
||||||
|
wp_enqueue_script( 'contact-form-js', ... );
|
||||||
|
wp_enqueue_style( 'contact-form-css', ... );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
// ✅ GOOD: Only load WooCommerce assets on shop pages.
|
||||||
|
add_action( 'wp_enqueue_scripts', function() {
|
||||||
|
if ( ! is_woocommerce() && ! is_cart() && ! is_checkout() ) {
|
||||||
|
wp_dequeue_style( 'woocommerce-general' );
|
||||||
|
wp_dequeue_script( 'wc-cart-fragments' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
```
|
||||||
|
|
||||||
|
### External API Requests
|
||||||
|
```php
|
||||||
|
// ❌ WARNING: No timeout set (default is 5 seconds).
|
||||||
|
wp_remote_get( $url ); // Set timeout: array( 'timeout' => 2 ).
|
||||||
|
|
||||||
|
// ❌ WARNING: Missing error handling for API failures.
|
||||||
|
$response = wp_remote_get( $url );
|
||||||
|
echo $response['body']; // Check is_wp_error() first!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sitemaps & Redirects
|
||||||
|
```php
|
||||||
|
// ❌ WARNING: Generating sitemaps for deep archives (crawlers hammer these).
|
||||||
|
// Solution: Exclude old post types, cache generated sitemaps.
|
||||||
|
|
||||||
|
// ❌ CRITICAL: Redirect loops consuming CPU.
|
||||||
|
// Debug with: x-redirect-by header, wp_debug_backtrace_summary().
|
||||||
|
```
|
||||||
|
|
||||||
|
### Post Meta Queries
|
||||||
|
```php
|
||||||
|
// ❌ WARNING: Searching meta_value without index.
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => 'color',
|
||||||
|
'value' => 'red',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Better: Use taxonomy or encode value in meta_key name.
|
||||||
|
|
||||||
|
// ❌ WARNING: Binary meta values requiring value scan.
|
||||||
|
'meta_key' => 'featured',
|
||||||
|
'meta_value' => 'true',
|
||||||
|
// Better: Presence of 'is_featured' key = true, absence = false.
|
||||||
|
```
|
||||||
|
|
||||||
|
**For deeper context on any pattern:** Load `references/anti-patterns.md`
|
||||||
|
|
||||||
|
## Severity Definitions
|
||||||
|
|
||||||
|
| Severity | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| **Critical** | Will cause failures at scale (OOM, 500 errors, DB locks) |
|
||||||
|
| **Warning** | Degrades performance under load |
|
||||||
|
| **Info** | Optimization opportunity |
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
Structure findings as:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Performance Review: [filename/component]
|
||||||
|
|
||||||
|
### Critical Issues
|
||||||
|
- **Line X**: [Issue] - [Explanation] - [Fix]
|
||||||
|
|
||||||
|
### Warnings
|
||||||
|
- **Line X**: [Issue] - [Explanation] - [Fix]
|
||||||
|
|
||||||
|
### Recommendations
|
||||||
|
- [Optimization opportunities]
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
- Total issues: X Critical, Y Warnings, Z Info
|
||||||
|
- Estimated impact: [High/Medium/Low]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
When performing performance reviews, avoid these errors:
|
||||||
|
|
||||||
|
| Mistake | Why It's Wrong | Fix |
|
||||||
|
|---------|----------------|-----|
|
||||||
|
| Flagging `posts_per_page => -1` in admin-only code | Admin queries don't face public scale | Check context - admin, CLI, cron are lower risk |
|
||||||
|
| Missing the `session_start()` buried in a plugin | Cache bypass affects entire site | Always grep for `session_start` across all code |
|
||||||
|
| Ignoring `no_found_rows` for non-paginated queries | Small optimization but adds up | Flag as INFO, not WARNING |
|
||||||
|
| Recommending object cache on shared hosting | Many shared hosts lack persistent cache | Check hosting environment first |
|
||||||
|
| Only reviewing PHP, missing JS polling | JS `setInterval` + fetch = self-DDoS | Review `.js` files for polling patterns |
|
||||||
|
|
||||||
|
## Deep-Dive References
|
||||||
|
|
||||||
|
Load these references based on the task:
|
||||||
|
|
||||||
|
| Task | Reference to Load |
|
||||||
|
|------|-------------------|
|
||||||
|
| Reviewing PHP code for issues | `references/anti-patterns.md` |
|
||||||
|
| Optimizing WP_Query calls | `references/wp-query-guide.md` |
|
||||||
|
| Implementing caching | `references/caching-guide.md` |
|
||||||
|
| High-traffic event prep | `references/measurement-guide.md` |
|
||||||
|
|
||||||
|
**Note**: For standard code reviews, `anti-patterns.md` contains all patterns needed. Other references provide deeper context when specifically optimizing queries, implementing caching strategies, or preparing for traffic events.
|
||||||
1199
skills/wp-performance-review/references/anti-patterns.md
Normal file
1199
skills/wp-performance-review/references/anti-patterns.md
Normal file
File diff suppressed because it is too large
Load Diff
447
skills/wp-performance-review/references/caching-guide.md
Normal file
447
skills/wp-performance-review/references/caching-guide.md
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
# WordPress Caching Strategy Guide
|
||||||
|
|
||||||
|
Comprehensive guide to caching strategies for high-performance WordPress applications.
|
||||||
|
|
||||||
|
## Caching Review Checklist
|
||||||
|
|
||||||
|
When reviewing caching implementation:
|
||||||
|
|
||||||
|
**Page Cache Compatibility**
|
||||||
|
- [ ] No `session_start()` on frontend
|
||||||
|
- [ ] No `setcookie()` on public pages (unless necessary)
|
||||||
|
- [ ] No `$_SESSION` usage on cacheable pages
|
||||||
|
- [ ] POST used only for writes, GET for reads
|
||||||
|
|
||||||
|
**Object Cache Usage**
|
||||||
|
- [ ] Expensive queries wrapped with `wp_cache_get`/`wp_cache_set`
|
||||||
|
- [ ] Cache keys include relevant variables (locale, user role if needed)
|
||||||
|
- [ ] TTL set appropriately (not too long, not too short)
|
||||||
|
- [ ] `wp_cache_get_multiple` for batch lookups
|
||||||
|
- [ ] Cache invalidation on relevant `save_post` hooks
|
||||||
|
|
||||||
|
**Race Condition Prevention**
|
||||||
|
- [ ] `wp_cache_add` used for locking when needed
|
||||||
|
- [ ] Stale-while-revalidate for high-traffic cached items
|
||||||
|
- [ ] Cache pre-warming via cron for critical data
|
||||||
|
|
||||||
|
## Caching Layers Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
User Request
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ CDN / Edge │ ← Full page cache (HTML)
|
||||||
|
│ Cache │ Static assets (JS, CSS, images)
|
||||||
|
└────────┬────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Page Cache │ ← Full page cache (server-level)
|
||||||
|
│ (Varnish, etc) │ Bypassed by: cookies, POST, query vars
|
||||||
|
└────────┬────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Object Cache │ ← Database query results
|
||||||
|
│ (Redis/Memcached)│ Transients, options, computed data
|
||||||
|
└────────┬────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Database │ ← MySQL query cache (if enabled)
|
||||||
|
│ (MySQL) │ InnoDB buffer pool
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Page Cache
|
||||||
|
|
||||||
|
### Cache Headers
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Set cache-friendly headers
|
||||||
|
function set_cache_headers() {
|
||||||
|
if (!is_user_logged_in() && !is_admin()) {
|
||||||
|
header('Cache-Control: public, max-age=300, s-maxage=3600');
|
||||||
|
header('Vary: Accept-Encoding');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action('send_headers', 'set_cache_headers');
|
||||||
|
|
||||||
|
// Prevent caching for dynamic pages
|
||||||
|
function prevent_page_cache() {
|
||||||
|
if (is_user_specific_page()) {
|
||||||
|
header('Cache-Control: private, no-cache, no-store');
|
||||||
|
nocache_headers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Bypass Triggers (Avoid These)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ Starting PHP sessions bypasses cache
|
||||||
|
session_start(); // Avoid on frontend
|
||||||
|
|
||||||
|
// ❌ Unique query parameters create cache misses
|
||||||
|
// https://example.com/?utm_source=twitter&utm_campaign=123
|
||||||
|
// Solution: Strip marketing params at CDN level
|
||||||
|
|
||||||
|
// ❌ POST requests always bypass cache
|
||||||
|
// Use GET for read operations
|
||||||
|
|
||||||
|
// ❌ Setting cookies prevents caching
|
||||||
|
setcookie('my_cookie', 'value'); // Use sparingly
|
||||||
|
```
|
||||||
|
|
||||||
|
### TTL (Time To Live) Strategy
|
||||||
|
|
||||||
|
| Content Type | Recommended TTL |
|
||||||
|
|--------------|-----------------|
|
||||||
|
| Homepage | 5-15 minutes |
|
||||||
|
| Archive pages | 15-60 minutes |
|
||||||
|
| Single posts | 1-24 hours |
|
||||||
|
| Static pages | 24+ hours |
|
||||||
|
| Media files | 1 year (versioned) |
|
||||||
|
|
||||||
|
## Object Cache
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Store data
|
||||||
|
wp_cache_set('my_key', $data, 'my_group', 3600); // 1 hour expiry
|
||||||
|
|
||||||
|
// Retrieve data
|
||||||
|
$data = wp_cache_get('my_key', 'my_group');
|
||||||
|
if (false === $data) {
|
||||||
|
$data = expensive_computation();
|
||||||
|
wp_cache_set('my_key', $data, 'my_group', 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete data
|
||||||
|
wp_cache_delete('my_key', 'my_group');
|
||||||
|
|
||||||
|
// Add only if doesn't exist (atomic)
|
||||||
|
$added = wp_cache_add('my_key', $data, 'my_group', 3600);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Operations (Efficient)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ BAD: Multiple round-trips
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$data[$id] = wp_cache_get("item_$id", 'items');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Single round-trip
|
||||||
|
$keys = array_map(fn($id) => "item_$id", $ids);
|
||||||
|
$data = wp_cache_get_multiple($keys, 'items');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Groups
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Use groups to organize and bulk-delete
|
||||||
|
wp_cache_set('post_123', $data, 'my_plugin_posts');
|
||||||
|
wp_cache_set('post_456', $data, 'my_plugin_posts');
|
||||||
|
|
||||||
|
// Clear entire group (if supported by backend)
|
||||||
|
wp_cache_flush_group('my_plugin_posts'); // Redis supports this
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Key Versioning
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Version cache keys for easy invalidation
|
||||||
|
function get_cache_key($base) {
|
||||||
|
$version = wp_cache_get('cache_version', 'my_plugin') ?: 1;
|
||||||
|
return "{$base}_v{$version}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate all by incrementing version
|
||||||
|
function invalidate_all_cache() {
|
||||||
|
wp_cache_incr('cache_version', 1, 'my_plugin');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Race Conditions
|
||||||
|
|
||||||
|
### Problem: Concurrent Cache Regeneration
|
||||||
|
|
||||||
|
When cache expires, multiple requests may simultaneously regenerate it.
|
||||||
|
|
||||||
|
```
|
||||||
|
Request A ─┬─ Cache miss ─→ Start regeneration ───────→ Set cache
|
||||||
|
│
|
||||||
|
Request B ─┴─ Cache miss ─→ Start regeneration ───────→ Set cache (duplicate!)
|
||||||
|
│
|
||||||
|
Request C ─┴─ Cache miss ─→ Start regeneration ───────→ Set cache (duplicate!)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solution: Locking Pattern
|
||||||
|
|
||||||
|
```php
|
||||||
|
function get_expensive_data() {
|
||||||
|
$cache_key = 'expensive_data';
|
||||||
|
$lock_key = 'expensive_data_lock';
|
||||||
|
|
||||||
|
// Try to get cached data
|
||||||
|
$data = wp_cache_get($cache_key);
|
||||||
|
if (false !== $data) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to acquire lock (atomic operation)
|
||||||
|
$lock_acquired = wp_cache_add($lock_key, true, '', 30); // 30 second lock
|
||||||
|
|
||||||
|
if ($lock_acquired) {
|
||||||
|
// We got the lock - regenerate cache
|
||||||
|
$data = expensive_computation();
|
||||||
|
wp_cache_set($cache_key, $data, '', 3600);
|
||||||
|
wp_cache_delete($lock_key); // Release lock
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another process is regenerating - wait and retry
|
||||||
|
usleep(100000); // 100ms
|
||||||
|
return get_expensive_data(); // Retry (add max retries in production)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solution: Stale-While-Revalidate
|
||||||
|
|
||||||
|
```php
|
||||||
|
function get_data_with_stale() {
|
||||||
|
$cache_key = 'my_data';
|
||||||
|
$stale_key = 'my_data_stale';
|
||||||
|
|
||||||
|
$data = wp_cache_get($cache_key);
|
||||||
|
if (false !== $data) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get stale data while regenerating
|
||||||
|
$stale_data = wp_cache_get($stale_key);
|
||||||
|
|
||||||
|
// Regenerate in background (non-blocking)
|
||||||
|
if (false === $stale_data) {
|
||||||
|
// No stale data - must wait
|
||||||
|
$data = regenerate_data();
|
||||||
|
wp_cache_set($cache_key, $data, '', 300);
|
||||||
|
wp_cache_set($stale_key, $data, '', 3600); // Keep stale longer
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule background regeneration
|
||||||
|
wp_schedule_single_event(time(), 'regenerate_my_data');
|
||||||
|
|
||||||
|
// Return stale data immediately
|
||||||
|
return $stale_data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache Stampede Prevention
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Cache expires → Thousands of requests hit database simultaneously.
|
||||||
|
|
||||||
|
### Solution 1: Jitter (Randomized Expiry)
|
||||||
|
|
||||||
|
```php
|
||||||
|
function set_cache_with_jitter($key, $data, $base_ttl) {
|
||||||
|
// Add ±10% randomization to TTL
|
||||||
|
$jitter = rand(-($base_ttl * 0.1), $base_ttl * 0.1);
|
||||||
|
$ttl = $base_ttl + $jitter;
|
||||||
|
wp_cache_set($key, $data, '', $ttl);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solution 2: Pre-warming via Cron
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Regenerate cache before it expires
|
||||||
|
add_action('pre_warm_popular_caches', function() {
|
||||||
|
$popular_queries = ['homepage_posts', 'featured_products', 'menu_items'];
|
||||||
|
|
||||||
|
foreach ($popular_queries as $query) {
|
||||||
|
// Force regeneration
|
||||||
|
wp_cache_delete($query);
|
||||||
|
get_cached_query($query); // Regenerates and caches
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule to run before typical expiry
|
||||||
|
if (!wp_next_scheduled('pre_warm_popular_caches')) {
|
||||||
|
wp_schedule_event(time(), 'hourly', 'pre_warm_popular_caches');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solution 3: Early Expiry Check
|
||||||
|
|
||||||
|
```php
|
||||||
|
function get_with_early_expiry($key, $ttl, $regenerate_callback) {
|
||||||
|
$data = wp_cache_get($key);
|
||||||
|
|
||||||
|
if (false !== $data) {
|
||||||
|
// Check if we should pre-regenerate (last 10% of TTL)
|
||||||
|
$meta = wp_cache_get("{$key}_meta");
|
||||||
|
if ($meta && (time() - $meta['created']) > ($ttl * 0.9)) {
|
||||||
|
// Trigger background regeneration
|
||||||
|
wp_schedule_single_event(time(), 'regenerate_cache', [$key]);
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss - regenerate
|
||||||
|
$data = call_user_func($regenerate_callback);
|
||||||
|
wp_cache_set($key, $data, '', $ttl);
|
||||||
|
wp_cache_set("{$key}_meta", ['created' => time()], '', $ttl);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transients
|
||||||
|
|
||||||
|
### When to Use Transients
|
||||||
|
|
||||||
|
- Data with known expiration
|
||||||
|
- External API responses
|
||||||
|
- Computed data that's expensive to regenerate
|
||||||
|
|
||||||
|
### Transient Best Practices
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ GOOD: Named clearly, reasonable TTL
|
||||||
|
set_transient('weather_data_seattle', $data, HOUR_IN_SECONDS);
|
||||||
|
|
||||||
|
// ❌ BAD: Dynamic keys create transient bloat
|
||||||
|
set_transient("user_{$user_id}_preferences", $data, DAY_IN_SECONDS);
|
||||||
|
// Better: Use user meta or object cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transient Storage Warning
|
||||||
|
|
||||||
|
Without persistent object cache, transients are stored in wp_options table:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Check if object cache is available
|
||||||
|
if (wp_using_ext_object_cache()) {
|
||||||
|
// Transients use object cache (good)
|
||||||
|
set_transient('my_data', $data, HOUR_IN_SECONDS);
|
||||||
|
} else {
|
||||||
|
// Transients go to database (potentially bad)
|
||||||
|
// Consider: file cache, skip caching, or warn admin
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Partial Output Caching
|
||||||
|
|
||||||
|
Cache rendered HTML fragments for repeated use:
|
||||||
|
|
||||||
|
```php
|
||||||
|
function get_cached_sidebar() {
|
||||||
|
$cache_key = 'sidebar_html_' . get_locale();
|
||||||
|
$html = wp_cache_get($cache_key, 'partials');
|
||||||
|
|
||||||
|
if (false === $html) {
|
||||||
|
ob_start();
|
||||||
|
get_sidebar();
|
||||||
|
$html = ob_get_clean();
|
||||||
|
wp_cache_set($cache_key, $html, 'partials', HOUR_IN_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## In-Memory Caching (Request-Scoped)
|
||||||
|
|
||||||
|
### Static Variables
|
||||||
|
|
||||||
|
```php
|
||||||
|
function get_current_user_data() {
|
||||||
|
static $user_data = null;
|
||||||
|
|
||||||
|
if (null === $user_data) {
|
||||||
|
$user_data = expensive_user_query();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user_data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Variable Pattern
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Store request-scoped data
|
||||||
|
global $my_plugin_cache;
|
||||||
|
$my_plugin_cache = [];
|
||||||
|
|
||||||
|
function get_item($id) {
|
||||||
|
global $my_plugin_cache;
|
||||||
|
|
||||||
|
if (!isset($my_plugin_cache[$id])) {
|
||||||
|
$my_plugin_cache[$id] = fetch_item($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $my_plugin_cache[$id];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache Invalidation Strategy
|
||||||
|
|
||||||
|
### Event-Based Invalidation
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Clear post-related caches when post is updated
|
||||||
|
add_action('save_post', function($post_id, $post) {
|
||||||
|
// Clear specific post cache
|
||||||
|
wp_cache_delete("post_{$post_id}", 'posts');
|
||||||
|
|
||||||
|
// Clear related caches
|
||||||
|
wp_cache_delete('recent_posts', 'listings');
|
||||||
|
wp_cache_delete('homepage_posts', 'listings');
|
||||||
|
|
||||||
|
// Clear category archive caches
|
||||||
|
$categories = wp_get_post_categories($post_id);
|
||||||
|
foreach ($categories as $cat_id) {
|
||||||
|
wp_cache_delete("category_{$cat_id}_posts", 'archives');
|
||||||
|
}
|
||||||
|
}, 10, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tag-Based Invalidation (Advanced)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Store cache tags
|
||||||
|
function set_tagged_cache($key, $data, $tags, $ttl) {
|
||||||
|
wp_cache_set($key, $data, '', $ttl);
|
||||||
|
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
$tag_keys = wp_cache_get("tag_{$tag}", 'cache_tags') ?: [];
|
||||||
|
$tag_keys[] = $key;
|
||||||
|
wp_cache_set("tag_{$tag}", array_unique($tag_keys), 'cache_tags');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate by tag
|
||||||
|
function invalidate_tag($tag) {
|
||||||
|
$keys = wp_cache_get("tag_{$tag}", 'cache_tags') ?: [];
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
wp_cache_delete($key);
|
||||||
|
}
|
||||||
|
wp_cache_delete("tag_{$tag}", 'cache_tags');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memcached vs Redis
|
||||||
|
|
||||||
|
| Feature | Memcached | Redis |
|
||||||
|
|---------|-----------|-------|
|
||||||
|
| Speed | Slightly faster | Fast |
|
||||||
|
| Data types | String only | Strings, lists, sets, hashes |
|
||||||
|
| Persistence | No | Optional |
|
||||||
|
| Memory efficiency | Higher | Lower |
|
||||||
|
| Cache groups | Limited | Full support |
|
||||||
|
| Complexity | Simple | More features |
|
||||||
|
|
||||||
|
**Recommendation**: Use what your host provides. Memcached for simple caching, Redis if you need advanced features.
|
||||||
356
skills/wp-performance-review/references/measurement-guide.md
Normal file
356
skills/wp-performance-review/references/measurement-guide.md
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
# High-Traffic Event Preparation & Performance Measurement
|
||||||
|
|
||||||
|
Guide for preparing WordPress sites for traffic surges and measuring performance.
|
||||||
|
|
||||||
|
## When to Use This Reference
|
||||||
|
|
||||||
|
This guide is for:
|
||||||
|
- **Pre-launch audits** - Checklist before high-traffic events
|
||||||
|
- **Performance testing** - How to load test with K6
|
||||||
|
- **Monitoring setup** - Query Monitor, New Relic integration
|
||||||
|
- **Alert configuration** - Threshold recommendations
|
||||||
|
|
||||||
|
**For code review**, use `anti-patterns.md` instead. This guide covers operational preparation.
|
||||||
|
|
||||||
|
## Code-Level Pre-Event Checks
|
||||||
|
|
||||||
|
Before a high-traffic event, scan the codebase for:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Things that WILL break under load
|
||||||
|
grep -rn "posts_per_page.*-1" . # Unbounded queries
|
||||||
|
grep -rn "session_start" . # Cache bypass
|
||||||
|
grep -rn "setInterval.*fetch\|setInterval.*ajax" . # Polling
|
||||||
|
|
||||||
|
# Things that DEGRADE under load
|
||||||
|
grep -rn "wp_remote_get\|wp_remote_post" . # Uncached HTTP
|
||||||
|
grep -rn "update_option\|add_option" . # DB writes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pre-Event Checklist
|
||||||
|
|
||||||
|
Before a high-traffic event (product launch, viral content, marketing campaign), verify:
|
||||||
|
|
||||||
|
### Cache Hit Rate
|
||||||
|
- [ ] **Page cache hit rate > 90%** - Check CDN/edge cache analytics
|
||||||
|
- [ ] **Object cache (memcached) hit rate > 90%** - Check cache stats
|
||||||
|
- [ ] **InnoDB buffer pool hit rate > 95%** - MySQL performance schema
|
||||||
|
- [ ] Identify pages consistently missing cache - optimize or exclude from event
|
||||||
|
|
||||||
|
### Database Health
|
||||||
|
- [ ] **No INSERT/UPDATE on uncached page loads** - Use Query Monitor
|
||||||
|
- Plugins persistently updating options = high DB CPU + binlog growth
|
||||||
|
- [ ] **Review P95 upstream response times** - Identify slowest endpoints
|
||||||
|
- High traffic to slow endpoints ties up PHP workers
|
||||||
|
- [ ] **Check for slow queries** - Queries > 100ms under normal load
|
||||||
|
- [ ] **Verify indexes** - Run EXPLAIN on common queries
|
||||||
|
|
||||||
|
### Error Monitoring
|
||||||
|
- [ ] **Clean PHP logs** - No fatal errors, minimize warnings
|
||||||
|
- Easier to debug issues if logs aren't full of noise
|
||||||
|
- [ ] **Zero 500 errors in 24h** - Check application error rate
|
||||||
|
- [ ] **Review error tracking** - New Relic, Sentry, etc.
|
||||||
|
|
||||||
|
### Load Testing
|
||||||
|
- [ ] **Run load test simulating expected traffic**
|
||||||
|
- [ ] **Test specific high-traffic URLs** - Homepage, landing pages
|
||||||
|
- [ ] **Monitor during test** - CPU, memory, DB connections, response times
|
||||||
|
|
||||||
|
## Traffic Pattern Types
|
||||||
|
|
||||||
|
### Sustained High Traffic on Multiple URLs
|
||||||
|
**Impact**: Strain on PHP workers, database connections, memory
|
||||||
|
**Preparation**:
|
||||||
|
- Scale horizontally (more app servers)
|
||||||
|
- Increase PHP worker count
|
||||||
|
- Optimize database queries
|
||||||
|
- Implement aggressive caching
|
||||||
|
|
||||||
|
### Large Spikes on Few URLs
|
||||||
|
**Impact**: Bottleneck on specific endpoints, potential cache stampede
|
||||||
|
**Preparation**:
|
||||||
|
- Pre-warm caches for target URLs
|
||||||
|
- Implement cache locking to prevent stampede
|
||||||
|
- Consider static HTML fallback for extreme cases
|
||||||
|
|
||||||
|
### Uncached Content Under Load
|
||||||
|
**Impact**: Every request hits origin, database overload
|
||||||
|
**Preparation**:
|
||||||
|
- Identify why pages bypass cache (cookies, sessions, query params)
|
||||||
|
- Implement partial caching for personalized pages
|
||||||
|
- Use ESI (Edge Side Includes) for dynamic fragments
|
||||||
|
|
||||||
|
## Load Testing with K6
|
||||||
|
|
||||||
|
K6 is an open-source load testing tool for simulating traffic.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
brew install k6
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
sudo apt-get install k6
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker run -i grafana/k6 run - <script.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Load Test Script
|
||||||
|
```javascript
|
||||||
|
// load-test.js
|
||||||
|
import http from 'k6/http';
|
||||||
|
import { check, sleep } from 'k6';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
vus: 100, // 100 virtual users
|
||||||
|
duration: '5m', // 5 minute test
|
||||||
|
thresholds: {
|
||||||
|
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
|
||||||
|
http_req_failed: ['rate<0.01'], // Less than 1% failures
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
// Test homepage
|
||||||
|
let homeResponse = http.get('https://example.com/');
|
||||||
|
check(homeResponse, {
|
||||||
|
'homepage status 200': (r) => r.status === 200,
|
||||||
|
'homepage < 500ms': (r) => r.timings.duration < 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test archive page
|
||||||
|
let archiveResponse = http.get('https://example.com/category/news/');
|
||||||
|
check(archiveResponse, {
|
||||||
|
'archive status 200': (r) => r.status === 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep(1); // Wait 1 second between iterations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Load Tests
|
||||||
|
```bash
|
||||||
|
# Basic run
|
||||||
|
k6 run load-test.js
|
||||||
|
|
||||||
|
# With more virtual users
|
||||||
|
k6 run --vus 200 --duration 10m load-test.js
|
||||||
|
|
||||||
|
# Output to JSON for analysis
|
||||||
|
k6 run --out json=results.json load-test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Metrics to Monitor
|
||||||
|
| Metric | Good | Concerning |
|
||||||
|
|--------|------|------------|
|
||||||
|
| `http_req_duration` (p95) | < 500ms | > 1000ms |
|
||||||
|
| `http_req_failed` | < 1% | > 5% |
|
||||||
|
| `http_reqs` (throughput) | Stable | Declining under load |
|
||||||
|
| `vus` vs response time | Linear | Exponential degradation |
|
||||||
|
|
||||||
|
## Measuring with Query Monitor
|
||||||
|
|
||||||
|
Query Monitor plugin provides detailed performance insights per page.
|
||||||
|
|
||||||
|
### Key Panels
|
||||||
|
|
||||||
|
**Overview Panel**
|
||||||
|
- Total page generation time
|
||||||
|
- Database query time
|
||||||
|
- HTTP API call time
|
||||||
|
|
||||||
|
**Database Queries Panel**
|
||||||
|
- Individual query times
|
||||||
|
- Duplicate queries (same query run multiple times)
|
||||||
|
- Slow queries highlighted
|
||||||
|
- Query origin (which plugin/theme)
|
||||||
|
|
||||||
|
**Object Cache Panel**
|
||||||
|
- Cache hit/miss ratio
|
||||||
|
- Total cache operations
|
||||||
|
- Cache groups usage
|
||||||
|
|
||||||
|
### Custom Timing with Query Monitor
|
||||||
|
```php
|
||||||
|
// Start timer
|
||||||
|
do_action('qm/start', 'my_operation');
|
||||||
|
|
||||||
|
// Your code here
|
||||||
|
expensive_operation();
|
||||||
|
|
||||||
|
// Stop timer
|
||||||
|
do_action('qm/stop', 'my_operation');
|
||||||
|
|
||||||
|
// For loops, use lap:
|
||||||
|
do_action('qm/start', 'loop_operation');
|
||||||
|
foreach ($items as $item) {
|
||||||
|
process_item($item);
|
||||||
|
do_action('qm/lap', 'loop_operation');
|
||||||
|
}
|
||||||
|
do_action('qm/stop', 'loop_operation');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Target Metrics in Query Monitor
|
||||||
|
| Metric | Target | Investigate |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Page generation | < 200ms | > 500ms |
|
||||||
|
| Database queries | < 50 | > 100 |
|
||||||
|
| Duplicate queries | 0 | > 5 |
|
||||||
|
| Slowest query | < 50ms | > 100ms |
|
||||||
|
| Object cache hits | > 90% | < 80% |
|
||||||
|
|
||||||
|
## Measuring with PHP Logging
|
||||||
|
|
||||||
|
Manual timing for specific code sections.
|
||||||
|
|
||||||
|
### Basic Timing
|
||||||
|
```php
|
||||||
|
function my_function() {
|
||||||
|
$start_time = microtime(true);
|
||||||
|
|
||||||
|
// Code to measure
|
||||||
|
expensive_operation();
|
||||||
|
|
||||||
|
$end_time = microtime(true);
|
||||||
|
$execution_time = $end_time - $start_time;
|
||||||
|
|
||||||
|
error_log(sprintf(
|
||||||
|
'my_function execution time: %.4f seconds',
|
||||||
|
$execution_time
|
||||||
|
));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timing Wrapper Function
|
||||||
|
```php
|
||||||
|
function measure_execution($callback, $label) {
|
||||||
|
$start = microtime(true);
|
||||||
|
$result = $callback();
|
||||||
|
$duration = microtime(true) - $start;
|
||||||
|
|
||||||
|
if ($duration > 0.1) { // Log if > 100ms
|
||||||
|
error_log(sprintf('[SLOW] %s: %.4fs', $label, $duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
$data = measure_execution(function() {
|
||||||
|
return expensive_query();
|
||||||
|
}, 'expensive_query');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Measurement
|
||||||
|
```php
|
||||||
|
$mem_start = memory_get_usage();
|
||||||
|
|
||||||
|
// Code that might use lots of memory
|
||||||
|
$large_array = process_data();
|
||||||
|
|
||||||
|
$mem_end = memory_get_usage();
|
||||||
|
$mem_used = ($mem_end - $mem_start) / 1024 / 1024;
|
||||||
|
|
||||||
|
error_log(sprintf('Memory used: %.2f MB', $mem_used));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Measuring with New Relic
|
||||||
|
|
||||||
|
New Relic provides APM (Application Performance Monitoring) for production.
|
||||||
|
|
||||||
|
### Custom Transaction Naming
|
||||||
|
```php
|
||||||
|
// Name transactions for better grouping
|
||||||
|
if (function_exists('newrelic_name_transaction')) {
|
||||||
|
if (is_single()) {
|
||||||
|
newrelic_name_transaction('single-post');
|
||||||
|
} elseif (is_archive()) {
|
||||||
|
newrelic_name_transaction('archive');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Instrumentation
|
||||||
|
```php
|
||||||
|
// Track custom code segments
|
||||||
|
if (function_exists('newrelic_start_transaction')) {
|
||||||
|
newrelic_start_transaction('my_custom_process');
|
||||||
|
|
||||||
|
// Your code
|
||||||
|
process_data();
|
||||||
|
|
||||||
|
newrelic_end_transaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom attributes for filtering
|
||||||
|
if (function_exists('newrelic_add_custom_parameter')) {
|
||||||
|
newrelic_add_custom_parameter('post_type', get_post_type());
|
||||||
|
newrelic_add_custom_parameter('user_role', $current_user_role);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key New Relic Metrics
|
||||||
|
- **Apdex score** - User satisfaction (target: > 0.9)
|
||||||
|
- **Web transaction time** - Average response time
|
||||||
|
- **Throughput** - Requests per minute
|
||||||
|
- **Error rate** - Percentage of failed requests
|
||||||
|
- **Database time** - Time spent in DB queries
|
||||||
|
|
||||||
|
## Performance Alerting
|
||||||
|
|
||||||
|
Set up alerts for performance degradation.
|
||||||
|
|
||||||
|
### Key Alert Thresholds
|
||||||
|
| Metric | Warning | Critical |
|
||||||
|
|--------|---------|----------|
|
||||||
|
| Response time (p95) | > 1s | > 3s |
|
||||||
|
| Error rate | > 1% | > 5% |
|
||||||
|
| CPU usage | > 70% | > 90% |
|
||||||
|
| Memory usage | > 80% | > 95% |
|
||||||
|
| Database connections | > 80% of max | > 95% of max |
|
||||||
|
|
||||||
|
### Alert Channels
|
||||||
|
- **Immediate**: PagerDuty, Slack, SMS for critical
|
||||||
|
- **Batched**: Email digest for warnings
|
||||||
|
- **Dashboard**: Real-time visibility for operations team
|
||||||
|
|
||||||
|
## Performance Regression Detection
|
||||||
|
|
||||||
|
Catch performance issues before production.
|
||||||
|
|
||||||
|
### Baseline Metrics
|
||||||
|
Establish baseline performance metrics:
|
||||||
|
```
|
||||||
|
Homepage: < 200ms, < 30 queries
|
||||||
|
Archive: < 300ms, < 50 queries
|
||||||
|
Single: < 250ms, < 40 queries
|
||||||
|
Search: < 500ms (with ElasticSearch)
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD Integration
|
||||||
|
```yaml
|
||||||
|
# Example: Run performance check in CI
|
||||||
|
- name: Performance Check
|
||||||
|
run: |
|
||||||
|
RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}' https://staging.example.com/)
|
||||||
|
if (( $(echo "$RESPONSE_TIME > 0.5" | bc -l) )); then
|
||||||
|
echo "Warning: Response time ${RESPONSE_TIME}s exceeds threshold"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Count Monitoring
|
||||||
|
```php
|
||||||
|
// Add to theme's functions.php for staging
|
||||||
|
add_action('shutdown', function() {
|
||||||
|
if (!defined('SAVEQUERIES') || !SAVEQUERIES) return;
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$query_count = count($wpdb->queries);
|
||||||
|
|
||||||
|
if ($query_count > 100) {
|
||||||
|
error_log("[PERFORMANCE] High query count: $query_count");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
328
skills/wp-performance-review/references/wp-query-guide.md
Normal file
328
skills/wp-performance-review/references/wp-query-guide.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# WP_Query Optimization Guide
|
||||||
|
|
||||||
|
Best practices for efficient database queries in WordPress applications.
|
||||||
|
|
||||||
|
## WP_Query Review Checklist
|
||||||
|
|
||||||
|
When reviewing WP_Query code, verify:
|
||||||
|
|
||||||
|
- [ ] `posts_per_page` is set (not -1, not missing)
|
||||||
|
- [ ] `no_found_rows => true` if not paginating
|
||||||
|
- [ ] `fields => 'ids'` if only IDs needed
|
||||||
|
- [ ] `update_post_meta_cache => false` if meta not used
|
||||||
|
- [ ] `update_post_term_cache => false` if terms not used
|
||||||
|
- [ ] Date limits on archive queries (recent content only)
|
||||||
|
- [ ] `include_children => false` if child terms not needed
|
||||||
|
- [ ] No `post__not_in` with large arrays
|
||||||
|
- [ ] No `meta_query` on `meta_value` (use taxonomy or key presence)
|
||||||
|
- [ ] Results cached with `wp_cache_set` if repeated
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
1. **Always set limits** - Never use `posts_per_page => -1`
|
||||||
|
2. **Validate inputs** - Check IDs before querying
|
||||||
|
3. **Limit scope** - Use date ranges, taxonomies to narrow results
|
||||||
|
4. **Skip unnecessary work** - Use `no_found_rows`, `fields`
|
||||||
|
5. **Pre-fetch related data** - Avoid N+1 queries
|
||||||
|
6. **Cache results** - Use object cache for repeated queries
|
||||||
|
|
||||||
|
## Essential Query Arguments
|
||||||
|
|
||||||
|
### Limiting Results
|
||||||
|
|
||||||
|
```php
|
||||||
|
// REQUIRED: Always limit results
|
||||||
|
'posts_per_page' => 10,
|
||||||
|
|
||||||
|
// Skip counting total posts if not paginating
|
||||||
|
'no_found_rows' => true, // Skips SQL_CALC_FOUND_ROWS
|
||||||
|
|
||||||
|
// Return only IDs when you don't need full post objects
|
||||||
|
'fields' => 'ids',
|
||||||
|
|
||||||
|
// Suppress filters when you need raw results
|
||||||
|
'suppress_filters' => true,
|
||||||
|
```
|
||||||
|
|
||||||
|
### Date-Based Limiting
|
||||||
|
|
||||||
|
For sites with years of content, limit queries to relevant time ranges:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Limit to recent content
|
||||||
|
$query = new WP_Query([
|
||||||
|
'post_type' => 'post',
|
||||||
|
'posts_per_page' => 10,
|
||||||
|
'date_query' => [
|
||||||
|
[
|
||||||
|
'after' => '3 months ago',
|
||||||
|
'inclusive' => true
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Specific date range
|
||||||
|
'date_query' => [
|
||||||
|
'after' => '2024-01-01',
|
||||||
|
'before' => '2024-12-31'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Dynamic date filtering via pre_get_posts
|
||||||
|
add_action('pre_get_posts', function($query) {
|
||||||
|
if (!is_admin() && $query->is_main_query() && $query->is_category('news')) {
|
||||||
|
$query->set('date_query', [
|
||||||
|
'after' => date('Y-m-d', strtotime('-3 months'))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Taxonomy Query Optimization
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Exclude child terms to reduce query complexity
|
||||||
|
'tax_query' => [
|
||||||
|
[
|
||||||
|
'taxonomy' => 'category',
|
||||||
|
'field' => 'term_id',
|
||||||
|
'terms' => [6],
|
||||||
|
'include_children' => false // Important for performance
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
// Use term_id instead of slug (avoids extra lookup)
|
||||||
|
'field' => 'term_id', // Faster than 'slug' or 'name'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Meta Query Optimization
|
||||||
|
|
||||||
|
Meta queries are expensive - minimize usage:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ AVOID: Multiple meta conditions
|
||||||
|
'meta_query' => [
|
||||||
|
'relation' => 'AND',
|
||||||
|
['key' => 'color', 'value' => 'red'],
|
||||||
|
['key' => 'size', 'value' => 'large'],
|
||||||
|
['key' => 'price', 'compare' => '>=', 'value' => 100]
|
||||||
|
]
|
||||||
|
|
||||||
|
// ✅ BETTER: Use taxonomies for filterable attributes
|
||||||
|
// Register 'color' and 'size' as taxonomies instead
|
||||||
|
|
||||||
|
// ✅ ALTERNATIVE: Offload to ElasticSearch
|
||||||
|
// Configure ElasticPress to index post meta
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pre-fetching & Cache Priming
|
||||||
|
|
||||||
|
### Update Meta Cache in Bulk
|
||||||
|
|
||||||
|
```php
|
||||||
|
// After running WP_Query, prime the meta cache
|
||||||
|
$query = new WP_Query($args);
|
||||||
|
$post_ids = wp_list_pluck($query->posts, 'ID');
|
||||||
|
|
||||||
|
// Prime meta cache (single query vs N queries)
|
||||||
|
update_postmeta_cache($post_ids);
|
||||||
|
|
||||||
|
// Prime term cache
|
||||||
|
update_object_term_cache($post_ids, 'post');
|
||||||
|
|
||||||
|
// Prime user cache for authors
|
||||||
|
$author_ids = wp_list_pluck($query->posts, 'post_author');
|
||||||
|
cache_users($author_ids);
|
||||||
|
```
|
||||||
|
|
||||||
|
### WP_Query Built-in Cache Updates
|
||||||
|
|
||||||
|
```php
|
||||||
|
// These are on by default but be aware:
|
||||||
|
'update_post_meta_cache' => true, // Updates meta cache after query
|
||||||
|
'update_post_term_cache' => true, // Updates term cache after query
|
||||||
|
|
||||||
|
// Disable if you don't need meta/terms (saves queries)
|
||||||
|
'update_post_meta_cache' => false,
|
||||||
|
'update_post_term_cache' => false,
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching Query Results
|
||||||
|
|
||||||
|
### Object Cache for Repeated Queries
|
||||||
|
|
||||||
|
```php
|
||||||
|
function get_featured_posts() {
|
||||||
|
$cache_key = 'featured_posts_v1';
|
||||||
|
$posts = wp_cache_get($cache_key, 'my_plugin');
|
||||||
|
|
||||||
|
if (false === $posts) {
|
||||||
|
$query = new WP_Query([
|
||||||
|
'post_type' => 'post',
|
||||||
|
'posts_per_page' => 5,
|
||||||
|
'meta_key' => 'featured',
|
||||||
|
'meta_value' => '1'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$posts = $query->posts;
|
||||||
|
wp_cache_set($cache_key, $posts, 'my_plugin', HOUR_IN_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate when posts are updated
|
||||||
|
add_action('save_post', function($post_id) {
|
||||||
|
wp_cache_delete('featured_posts_v1', 'my_plugin');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static Variable Caching (Request-Scoped)
|
||||||
|
|
||||||
|
```php
|
||||||
|
function get_expensive_data($id) {
|
||||||
|
static $cache = [];
|
||||||
|
|
||||||
|
if (!isset($cache[$id])) {
|
||||||
|
$cache[$id] = expensive_computation($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cache[$id];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ElasticSearch Offloading
|
||||||
|
|
||||||
|
For complex searches, offload to ElasticSearch via ElasticPress:
|
||||||
|
|
||||||
|
### When to Offload
|
||||||
|
|
||||||
|
- Full-text search queries
|
||||||
|
- Complex meta queries
|
||||||
|
- Faceted search / filtering
|
||||||
|
- Large result sets with sorting
|
||||||
|
- Aggregations / analytics
|
||||||
|
|
||||||
|
### When NOT to Offload
|
||||||
|
|
||||||
|
- Simple primary key lookups
|
||||||
|
- Transactional data requiring ACID
|
||||||
|
- Data requiring immediate consistency
|
||||||
|
- Small data sets
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ElasticPress automatically intercepts WP_Query
|
||||||
|
// for search queries when configured
|
||||||
|
|
||||||
|
// Force MySQL for specific queries
|
||||||
|
$query = new WP_Query([
|
||||||
|
'ep_integrate' => false, // Skip ElasticSearch
|
||||||
|
// ... args
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Analyzing Queries with EXPLAIN
|
||||||
|
|
||||||
|
Use EXPLAIN to identify slow queries:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
EXPLAIN SELECT * FROM wp_posts
|
||||||
|
WHERE post_type = 'post'
|
||||||
|
AND post_status = 'publish'
|
||||||
|
ORDER BY post_date DESC
|
||||||
|
LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key EXPLAIN Indicators
|
||||||
|
|
||||||
|
| Column | Good Value | Bad Value |
|
||||||
|
|--------|------------|-----------|
|
||||||
|
| `type` | `const`, `eq_ref`, `ref`, `range` | `ALL` (full table scan) |
|
||||||
|
| `key` | Named index | `NULL` (no index used) |
|
||||||
|
| `rows` | Small number | Large number |
|
||||||
|
| `Extra` | `Using index` | `Using filesort`, `Using temporary` |
|
||||||
|
|
||||||
|
### Common Optimization Actions
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Add index for frequently queried meta key
|
||||||
|
ALTER TABLE wp_postmeta ADD INDEX meta_key_value (meta_key, meta_value(50));
|
||||||
|
|
||||||
|
-- Add composite index
|
||||||
|
ALTER TABLE wp_posts ADD INDEX type_status_date (post_type, post_status, post_date);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Monitor Integration
|
||||||
|
|
||||||
|
Use Query Monitor plugin to identify:
|
||||||
|
|
||||||
|
1. **Slow queries** - Queries over threshold
|
||||||
|
2. **Duplicate queries** - Same query run multiple times
|
||||||
|
3. **Queries by component** - Which plugin/theme caused each query
|
||||||
|
4. **Query count** - Total queries per page load
|
||||||
|
|
||||||
|
### Target Metrics
|
||||||
|
|
||||||
|
| Metric | Target | Concern |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| Total queries | < 50 | > 100 |
|
||||||
|
| Duplicate queries | 0 | > 5 |
|
||||||
|
| Slowest query | < 50ms | > 100ms |
|
||||||
|
| Total query time | < 100ms | > 500ms |
|
||||||
|
|
||||||
|
## Common Query Patterns
|
||||||
|
|
||||||
|
### Get Latest Posts (Optimized)
|
||||||
|
|
||||||
|
```php
|
||||||
|
$query = new WP_Query([
|
||||||
|
'post_type' => 'post',
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => 10,
|
||||||
|
'no_found_rows' => true,
|
||||||
|
'update_post_term_cache' => false, // If not displaying categories
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Posts by IDs (Optimized)
|
||||||
|
|
||||||
|
```php
|
||||||
|
$query = new WP_Query([
|
||||||
|
'post__in' => $post_ids,
|
||||||
|
'posts_per_page' => count($post_ids),
|
||||||
|
'orderby' => 'post__in', // Preserve ID order
|
||||||
|
'no_found_rows' => true,
|
||||||
|
'ignore_sticky_posts' => true
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check if Posts Exist (Optimized)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Don't fetch full posts just to check existence
|
||||||
|
$query = new WP_Query([
|
||||||
|
'post_type' => 'product',
|
||||||
|
'posts_per_page' => 1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'no_found_rows' => true,
|
||||||
|
'update_post_meta_cache' => false,
|
||||||
|
'update_post_term_cache' => false
|
||||||
|
]);
|
||||||
|
$has_products = $query->have_posts();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Count Posts (Optimized)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Use wp_count_posts() for simple counts
|
||||||
|
$counts = wp_count_posts('post');
|
||||||
|
$published = $counts->publish;
|
||||||
|
|
||||||
|
// For filtered counts, use found_posts
|
||||||
|
$query = new WP_Query([
|
||||||
|
'post_type' => 'post',
|
||||||
|
'posts_per_page' => 1, // Minimize actual retrieval
|
||||||
|
'category_name' => 'news',
|
||||||
|
// no_found_rows must be FALSE to get found_posts
|
||||||
|
]);
|
||||||
|
$total = $query->found_posts;
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user