Skillshub wp-performance-backend
WordPress backend performance optimization — profiling, queries, object cache, autoload, cron, and remote HTTP. Always-active rules when investigating slowness issues.
git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/alessioarzenton/claude-code-wp-toolkit/wp-performance-backend" ~/.claude/skills/comeonoliver-skillshub-wp-performance-backend && rm -rf "$T"
skills/alessioarzenton/claude-code-wp-toolkit/wp-performance-backend/SKILL.mdWordPress Backend Performance
Skill for diagnosing and resolving backend performance issues in WordPress 6.x+. Complementary to the
performance skill (which covers frontend/Core Web Vitals). This one focuses on: server-side profiling, database queries, object cache, autoload options, cron, and remote HTTP calls.
When to use
Apply this skill when:
- A page, REST endpoint, or admin screen is slow (high TTFB)
- A profiling plan and tool recommendations are needed
- Optimizing DB queries, autoloaded options, object cache, cron tasks, or remote HTTP calls
- The symptom is intermittent or tied to logged-in vs anonymous users
Do not use for: frontend asset optimization (CSS/JS/images), Core Web Vitals, lazy loading — see the
performance skill.
Prerequisites
Before starting, verify:
- Environment: dev, staging, or production — and whether you can make changes (install plugins, change config)
- How to target the installation: WordPress root path (
for WP-CLI)--path=<path> - Symptom: which URL/REST route/admin screen is slow, when it happens (always vs sporadic, logged-in vs anonymous)
- WP-CLI available: required for advanced profiling commands
Procedure
0) Guardrails — measure first, avoid risky operations
- Confirm whether you can perform write operations (install plugins, change config, flush cache)
- Identify a reproducible target (URL or REST route) and capture a baseline:
# Baseline with curl (TTFB). curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" "https://esempio.test/bandi/" # With WP-CLI profile (if available). wp profile stage --url="https://esempio.test/bandi/"
- Do not enable
orSAVEQUERIES
in production without explicit approval — they cause significant overheadWP_DEBUG
1) Quick diagnostics (before deep profiling)
If you have WP-CLI with the
wp doctor plugin:
# Quick check — identifies common issues. wp doctor check # Issues it detects: # - Autoload bloat (autoloaded options > 1MB) # - SAVEQUERIES/WP_DEBUG active in production # - Too many active plugins # - Core/plugin/theme updates available
If you don't have
wp doctor, check manually:
# Autoload size. wp db query "SELECT SUM(LENGTH(option_value)) as total FROM $(wp db prefix)options WHERE autoload = 'yes'" --skip-column-names # Active plugin count. wp plugin list --status=active --format=count # WP and PHP version. wp core version && wp eval "echo PHP_VERSION;"
2) Deep profiling
Order of preference:
A) WP-CLI Profile (without browser)
# Where does the time go? (bootstrap / main_query / template). wp profile stage --url="https://esempio.test/bandi/" # Which hooks are slow? wp profile hook --url="https://esempio.test/bandi/" --spotlight # Test a specific code path. wp profile eval 'get_posts(["post_type" => "bando", "posts_per_page" => 50]);'
B) Query Monitor (headless usage via REST)
Query Monitor can be used without a browser via REST response headers:
# Authenticate and inspect x-qm-* headers. curl -s -D - "https://esempio.test/wp-json/wp/v2/posts" \ -H "Authorization: Basic $(echo -n 'user:app-password' | base64)" \ | grep -i 'x-qm'
C) Xdebug/XHProf + Blackfire
For PHP-level profiling — requires server configuration. Coordinate with hosting/DevOps.
3) Fixes by category (address the dominant bottleneck)
Use the profiling output to choose one main category:
A) Database queries
Common issues:
| Problem | Diagnosis | Solution |
|---|---|---|
| Too many queries | Query count > 50 per page | Identify N+1 patterns, use |
| Slow queries | Individual queries > 100ms | Add indexes, avoid complex |
Expensive | or on postmeta | Consider custom taxonomy instead of meta |
| N+1 on post meta | in loop without pre-cache | Use before the loop |
// BEFORE — N+1 pattern (1 query for each post in the loop). foreach ($posts as $post) { $stato = get_post_meta($post->ID, 'stato_bando', true); } // AFTER — Pre-cache + loop (2 total queries). update_postmeta_cache(wp_list_pluck($posts, 'ID')); foreach ($posts as $post) { $stato = get_post_meta($post->ID, 'stato_bando', true); }
B) Autoload options
Options with
autoload = 'yes' are loaded on every request:
# Identify the largest autoloaded options. wp db query "SELECT option_name, LENGTH(option_value) as size FROM $(wp db prefix)options WHERE autoload = 'yes' ORDER BY size DESC LIMIT 20"
- If you find blobs > 100KB: consider disabling autoload or moving the data
- Plugins like
,rewrite_rules
,active_plugins
are normally autoloadedwidget_*
// Disable autoload for a heavy option. wp_set_option_autoload('mia_opzione_pesante', false);
C) Object cache
If there is no persistent object cache (Redis, Memcached), the cache is in-memory only for the single request:
# Check if an object cache drop-in exists. wp eval "echo file_exists(WP_CONTENT_DIR . '/object-cache.php') ? 'YES' : 'NO';"
- Without persistent cache: every request re-runs all queries -> high impact on high-traffic sites
- With persistent cache: verify hit rate and key sizes
// Use wp_cache for data that is expensive to compute. $risultati = wp_cache_get('bandi_attivi_conteggio', '{{TEXT_DOMAIN}}'); if (false === $risultati) { $risultati = calcola_bandi_attivi(); wp_cache_set('bandi_attivi_conteggio', $risultati, '{{TEXT_DOMAIN}}', HOUR_IN_SECONDS); }
D) Remote HTTP calls
Calls to external APIs during page rendering are a performance killer:
- Add short timeouts:
(WordPress default: 5s, but some plugins increase it)'timeout' => 5 - Cache the responses with transients:
function get_dati_esterni(): array { $cached = get_transient('dati_api_esterna'); if (false !== $cached) { return $cached; } $response = wp_remote_get('https://api.esempio.it/dati', ['timeout' => 5]); if (is_wp_error($response)) { return []; // Graceful fallback. } $dati = json_decode(wp_remote_retrieve_body($response), true); set_transient('dati_api_esterna', $dati, HOUR_IN_SECONDS); return $dati; }
- Never call external APIs in a loop — use batch or a single request
E) Cron
# List scheduled cron events. wp cron event list # Run a single event for debugging. wp cron event run mio_evento_cron
- Reduce "due now" event spikes — distribute the schedules
- Avoid cron tasks that do heavy work in the HTTP request path
- For long-running tasks: consider
from the system (crontab) instead ofwp cron event runwp-cron.php
4) Verification (repeat the same measurement)
# Same measurement as the baseline. curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" "https://esempio.test/bandi/" # Or with WP-CLI. wp profile stage --url="https://esempio.test/bandi/"
- Compare numbers before/after (same environment, same URL)
- Verify that functional behavior has not changed
- If the fix is risky, release behind a feature flag or gradually
WordPress 6.9 — Performance improvements
Keep these changes in mind when profiling:
- On-demand CSS for classic themes: classic themes now load CSS only for blocks used on the page (previously only block themes). CSS payload reduction of 30-65%
- Block themes without render-blocking assets: themes like Twenty Twenty-Four can load with zero blocking CSS (inline styles from theme.json)
- Increased CSS inline threshold: more small stylesheets are inlined, reducing blocking requests
Verification checklist
- Baseline and post-fix measurements captured (same environment, same URL)
-
clean (or improved) if availablewp doctor check - No new PHP errors or warnings in the logs
- No cache flush needed for correctness (flushing should be a last resort)
- Functional behavior unchanged after optimization
- Query count reduced (goal: < 50 for a standard page)
Common errors and solutions
| Problem | Likely cause | Solution |
|---|---|---|
| No improvement after the fix | Measured different URL/site, cache masking the result, stale opcode cache | Verify targeting, flush OPcache, repeat |
| Noisy profiling data | Background tasks, cold caches, few samples | Eliminate variables, test with warm caches, take more measurements |
causes overhead | Enabled in production | Disable immediately — use only in dev/staging |
| Object cache not working | Missing drop-in or Redis/Memcached server down | Verify and cache server status |
What NOT to do
- Do not optimize without measuring first — profiling comes before the fix
- Do not enable
orSAVEQUERIES
in production without approvalWP_DEBUG - Do not install plugins or run load tests in production without coordination
- Do not flush cache during traffic without reason
- Do not add MySQL indexes blindly — verify with
firstEXPLAIN - Do not cache everything indiscriminately — invalid cache is worse than no cache