Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:25:33 +08:00
commit ec3986470d
10 changed files with 2945 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View 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.

View 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");
}
});
```

View 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;
```