| name | superpowers-sage:wp-performance |
| description | WordPress performance optimization: performance, slow query, N+1 query, Query Monitor, autoload audit, autoload options, object cache, Redis, wp_cache, transient, cache invalidation, Core Web Vitals, LCP, CLS, INP, FID, profiling, Xdebug, microtime, pre_get_posts, database query optimization, TTFB, page load, Vite bundle, lazy loading, critical CSS — benchmark before/after, never optimize blind. Invoke for: slow page load, N+1 query problem, caching strategy, transient API, dequeue unused scripts, performance profiling, Core Web Vitals improvement.
|
| user-invocable | false |
WordPress Performance Optimization
When to Use
- Page load times are slow or degrading
- Core Web Vitals scores are poor (LCP > 2.5s, INP > 200ms, CLS > 0.1)
- Database queries are excessive or slow (QM shows > 50 queries or queries > 50ms)
- Object cache hit rates are low (< 90%)
- Autoloaded options table is bloated (> 500KB total)
- N+1 query patterns detected in Eloquent models
- Hook execution is slow or excessive
- REST API responses are slow
Scripts
bash skills/wp-performance/scripts/query-monitor-dump.sh
bash skills/wp-performance/scripts/query-monitor-dump.sh https://mysite.lndo.site
bash skills/wp-performance/scripts/autoload-audit.sh
Scripts: scripts/query-monitor-dump.sh · scripts/autoload-audit.sh
Procedure
Step 1 — Install Query Monitor
Always profile before optimizing. Never optimize blind.
lando wp plugin install query-monitor --activate
If the db.php symlink fails (permissions):
lando ssh -c "ln -sf /app/wp/wp-content/plugins/query-monitor/wp-content/db.php /app/wp/wp-content/db.php"
Key constants (Bedrock config/application.php):
Config::define('QM_DB_EXPENSIVE', 0.05);
Config::define('QM_ENABLE_CAPS_PANEL', true);
See references/query-monitor.md for all panels,
QM timers, custom logging, and how to export data as JSON.
Step 2 — Identify the Bottleneck
Follow this isolation sequence:
- Bootstrap — plugins_loaded, Acorn boot, autoloaded options
- Main query — WP_Query execution time, parameter validation
- Template rendering — Blade, view composers, ACF block
with() queries
- Frontend — Vite bundle size, image loading, paint timing
QM toolbar shows: page generation time, peak memory, SQL duration, query count.
Step 3 — Fix the Top Issues
N+1 Queries (most common)
Enable lazy loading prevention in development:
Model::preventLazyLoading(! app()->isProduction());
Fix with eager loading:
$posts = Post::all();
foreach ($posts as $post) { echo $post->author->name; }
$posts = Post::with('author')->get();
For native WP meta loops:
update_post_meta_cache($postIds);
foreach ($postIds as $id) {
$price = get_post_meta($id, 'price', true);
}
See references/n-plus-one.md for IN query batching,
ACF block patterns, and full detection workflow.
Autoloaded Options Bloat
bash skills/wp-performance/scripts/autoload-audit.sh
Fix oversized options:
lando wp option update <option_name> --autoload=no
Target: total autoloaded size < 500KB.
See references/autoload.md for audit workflow, common plugin culprits, and when to set autoload=no.
Object Cache / Redis
lando redis-cli ping
Monitor hit rates in QM's Object Cache panel. Target: > 90%.
$posts = wp_cache_get("category-posts-{$id}", 'my-plugin');
if (false === $posts) {
$posts = get_posts(['category' => $id, 'posts_per_page' => 10]);
wp_cache_set("category-posts-{$id}", $posts, 'my-plugin', HOUR_IN_SECONDS);
}
Every cache write requires a documented invalidation strategy. Bust on save_post,
edited_term, or the relevant update hook.
See references/caching.md for transients, Cache::remember(),
full-page caching, and group invalidation.
Database Query Optimization
- Add indexes for columns in
WHERE, ORDER BY, JOIN
- Always use
$wpdb->prepare() for raw queries
- Avoid
SELECT * — specify columns
- Use Eloquent query scopes to encapsulate reusable logic
$wpdb->show_errors() in development only — never in production
Vite Bundle Size
lando theme-build -- --analyze
- Dynamic imports:
const m = await import('./heavy-module.js')
- Split large vendor libraries via
manualChunks in vite.config.js
- Target: no single chunk > 200KB gzipped
Core Web Vitals
| Metric | Target | Common Sage causes |
|---|
| LCP | < 2.5s | Large hero image, render-blocking CSS, slow TTFB |
| INP | < 200ms | Heavy JS bundle, long main-thread tasks, Livewire hydration |
| CLS | < 0.1 | Images without dimensions, dynamically injected content |
Always declare width and height on images. Preload the LCP image:
<link rel="preload" as="image" href="{{ $heroImageUrl }}" fetchpriority="high">
See references/core-web-vitals.md for full LCP/INP/CLS
remediation, critical CSS with Vite, and third-party script strategies.
Step 4 — Deep Profiling (if bottleneck unclear)
Use QM timers for targeted code sections:
do_action('qm/start', 'my-operation');
$result = $this->expensiveComputation();
do_action('qm/stop', 'my-operation');
For call-graph analysis, use Xdebug profiler in Lando:
services:
appserver:
xdebug: profile
Trigger: append ?XDEBUG_PROFILE=1 to the URL.
See references/profiling.md for Xdebug setup,
microtime() patterns, and WP hook profiling (pre_get_posts, save_post).
Cron: Action Scheduler vs wp-cron
- wp-cron: triggered by page visits, unreliable under low traffic
- Action Scheduler: queue-based, retries, logging, concurrent processing
- Development:
lando wp cron event run --due-now
- Production: disable wp-cron (
DISABLE_WP_CRON=true) and use real system cron
Critical Rules
- Check Query Monitor before optimizing. Profile first — never optimize blind.
- Benchmark before and after every change. Record QM query count, page time,
and memory before the fix; verify improvement after.
$wpdb->show_errors() only in development. Never leave error display enabled
in staging or production — it leaks sensitive schema information.
- Every cache write requires an invalidation strategy. Document the hook and
timing for every
wp_cache_set, set_transient, or Cache::remember().
- Do not use
$wpdb->show_errors() in production. Use QM logging instead.
Verification
Failure modes
| Symptom | Likely cause | Fix |
|---|
| TTFB > 1s | No object cache or slow queries | Enable Redis; audit autoloaded options |
| Query count > 50 per page | N+1 or missing eager loading | ->with(), update_post_meta_cache() |
| LCP > 4s | Large unoptimized hero image | Compress, WebP, preload hint |
| CLS > 0.25 | Images without explicit dimensions | Add width/height |
| Redis connection refused | Redis not running in Lando | Check .lando.yml, lando rebuild |
| Object cache not working | Drop-in not installed | Cross-ref acorn-redis skill |
| Stale cached data | No invalidation on update | Add wp_cache_delete to save hooks |
| QM toolbar not visible | Not logged in as admin | Set QM auth cookie via Settings |
| QM db.php symlink missing | Permissions issue | Manually symlink (see Step 1) |
| QM panels empty | QM_DISABLED is true | Check config/application.php constants |
| Slow hooks detected | Expensive callbacks on frequent hooks | Queue async; use QM timers to measure |
Escalation
- Performance issues persist after all fixes → profile with Xdebug or Blackfire
- Database bottlenecks beyond query optimization → DBA for schema/read replica
- CWV blocked by third-party scripts → document constraint, escalate to project lead
- Infrastructure-level issues (server response time, CDN) → hosting/DevOps