feat: add wordpress-performance-diagnostics skill
Paired skills.sh/autoskills.sh catalog scans (2026-08-15, reports in granja/_temp/codex-logs/) independently flagged wordpress/agent-skills' wp-performance module as the best net-new find of the whole scan: a measurement-first, backend-only WP diagnostic workflow (WP-CLI doctor/profile, headless Query Monitor, autoload/object-cache/cron checks) — a strong fit since this fleet's 13+ WordPress sites are all headless/jailed with no browser-first profiling access. Adapted from github.com/wordpress/agent-skills (skills/wp-performance) rather than installed verbatim: swapped the upstream's local/SSH assumptions for the fleet's actual `bastille cmd ... su -m www` remote-execution pattern (reusing wordpress-cli-remote-execution's convention exactly), added a jail/site selector step against the real granja/staging/gringo inventory, narrowed the always-collect baseline to four metrics (TTFB, autoload size, object-cache presence, cron health) with fleet-specific context for each, added redaction guidance for Valkey/DB credentials that diagnostic output can surface, and replaced the upstream's hardcoded "WordPress 7.0+" compatibility claim with per-site version verification. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,253 @@
|
|||||||
|
---
|
||||||
|
name: wordpress-performance-diagnostics
|
||||||
|
description: Use when investigating why a WordPress site in the fleet is slow (frontend TTFB, wp-admin, a REST route, WP-Cron) or before proposing any performance remediation on one. Backend-only, measurement-first diagnostic workflow for headless/jailed WordPress sites reached via `bastille cmd` — establishes baseline evidence (TTFB, autoloaded-options size, object-cache presence, cron health) before any write/remediation action.
|
||||||
|
---
|
||||||
|
|
||||||
|
# WordPress Performance Diagnostics
|
||||||
|
|
||||||
|
Adapted from wordpress/agent-skills' `wp-performance` module
|
||||||
|
(`github.com/wordpress/agent-skills`, `skills/wp-performance`) —
|
||||||
|
identified 2026-08-15 via a paired skills.sh/autoskills.sh catalog scan;
|
||||||
|
both scans independently flagged it as the single best net-new find of
|
||||||
|
the whole scan, since this fleet's WordPress estate is entirely
|
||||||
|
headless/jailed with no convenient browser-first profiling access. This
|
||||||
|
version keeps the upstream's measurement-first structure but replaces its
|
||||||
|
local/SSH assumptions with the fleet's actual `bastille cmd`
|
||||||
|
remote-execution pattern, adds a jail/site selector step (13+ sites
|
||||||
|
across granja/staging/gringo), and narrows the always-collect baseline to
|
||||||
|
four metrics with fleet-specific context for each.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
- A WordPress site (or several) is reported slow — frontend TTFB,
|
||||||
|
wp-admin, a REST route, or WP-Cron.
|
||||||
|
- Before proposing ANY performance remediation on a fleet site — collect
|
||||||
|
the baseline evidence first, always.
|
||||||
|
- Comparing before/after a change (theme deploy, plugin update,
|
||||||
|
object-cache rollout) to confirm it actually helped.
|
||||||
|
|
||||||
|
Every site in this fleet is a jailed, headless WordPress install with no
|
||||||
|
browser-based profiling access — WP-CLI, `curl`, and log inspection are
|
||||||
|
the only tools available. Treat this as backend-only by default.
|
||||||
|
|
||||||
|
## Step 0 — read-only-first, always
|
||||||
|
|
||||||
|
This skill's job is to produce evidence, not to remediate. Until the user
|
||||||
|
has explicitly asked for a fix (and you've already captured a baseline to
|
||||||
|
measure against), do not:
|
||||||
|
|
||||||
|
- flush any cache (`wp cache flush`, `wp redis flush`/`wp redis enable`)
|
||||||
|
- install or activate a plugin
|
||||||
|
- change `wp-config.php` or any debug constant (`WP_DEBUG`, `SAVEQUERIES`)
|
||||||
|
- restart php-fpm/nginx/valkey
|
||||||
|
|
||||||
|
If a fix is later proposed, re-run the exact same measurements from Step
|
||||||
|
3 afterward and report the delta — see Verification below. Flushing or
|
||||||
|
restarting anything *before* you have a baseline destroys the thing you
|
||||||
|
needed to compare against.
|
||||||
|
|
||||||
|
## Step 1 — identify the jail (site selector)
|
||||||
|
|
||||||
|
The fleet runs 13+ WordPress jails across three hosts. Domain names do
|
||||||
|
not map to jail names by a fixed rule (jail names are typically the
|
||||||
|
domain with punctuation stripped, but always confirm, don't guess) — look
|
||||||
|
it up in the per-host inventory before running anything:
|
||||||
|
|
||||||
|
- `docs/server-granja.md` — granja's "Site jails" table: 13 WordPress
|
||||||
|
jails, e.g. `menorca.pt` → jail `menorcapt` (`10.20.0.19`), `elrumano.es`
|
||||||
|
→ jail `elrumanoro` (`10.20.0.12`).
|
||||||
|
- `docs/server-staging.md` — staging's "Site jails" table, e.g. the
|
||||||
|
`bauxa` jail, which serves Cala Verd restaurant's WordPress site
|
||||||
|
alongside four other, non-WordPress services in the same jail.
|
||||||
|
- `docs/server-gringo.md` — gringo's site jails, e.g. `bauxa-app`
|
||||||
|
(`10.20.0.76`) — the same Cala Verd WordPress site, migrated here
|
||||||
|
2026-08-13 but not yet cut over in DNS/NPM; both copies are currently
|
||||||
|
live, so confirm which one you actually mean before diagnosing.
|
||||||
|
|
||||||
|
If a domain isn't in any of those tables, don't guess a jail name — ask,
|
||||||
|
or `ssh root@<host> "bastille list"` and cross-reference `wp option get
|
||||||
|
siteurl` per jail (the same method used for the 2026-08-13 granja
|
||||||
|
re-audit) rather than trusting an older or stale note.
|
||||||
|
|
||||||
|
Once you have `<host>`, `<jail>`, and the site's on-disk path, confirm the
|
||||||
|
path rather than assuming a fixed convention — e.g. `bastille cmd <jail>
|
||||||
|
ls /usr/local/www/`. Multi-service jails (like `bauxa-app`, which serves
|
||||||
|
five separate things behind one jail IP) don't have one obvious WordPress
|
||||||
|
docroot; guessing wrong here silently profiles the wrong document root.
|
||||||
|
|
||||||
|
## Step 2 — remote WP-CLI wrapper
|
||||||
|
|
||||||
|
Reuse the fleet's established remote-execution pattern exactly — see the
|
||||||
|
`wordpress-cli-remote-execution` skill for the full gotcha list
|
||||||
|
(jail-exec output-prefix stripping, `error_reporting(0)` bootstraps
|
||||||
|
swallowing errors, shell globbing failing silently under some remote
|
||||||
|
shells). Do not invent a parallel SSH-direct or local-WP-CLI pattern;
|
||||||
|
every site in this fleet is reached the same way:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@<host> "bastille cmd <jail> sh -c 'cd <site-path> && su -m www -c \"wp <command> --path=<site-path>\"'"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run as the jail's web user (`www`) via `su -m www -c`, not as root —
|
||||||
|
matches the fleet convention and avoids the `wp-content`
|
||||||
|
ownership-drift problem documented in `wordpress-plugin-conventions`
|
||||||
|
(files created as root over SSH have already caused a real
|
||||||
|
WordPress-core-update failure on this fleet — `menorca.ro`, 2026-07-10).
|
||||||
|
- Always pass `--path=` explicitly, don't rely on `cd` propagating.
|
||||||
|
- Strip the `bastille cmd` wrapper's own `[<jail>]:` prefix line before
|
||||||
|
parsing any captured output programmatically.
|
||||||
|
|
||||||
|
## Step 3 — the four core baseline metrics
|
||||||
|
|
||||||
|
Collect all four before proposing any fix, even when the symptom seems to
|
||||||
|
point at one obvious cause — a slow site is frequently more than one
|
||||||
|
thing at once, and an isolated fix without a baseline can't be verified
|
||||||
|
afterward.
|
||||||
|
|
||||||
|
**1. TTFB** (time to first byte, measured from outside the jail):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -o /dev/null -w 'ttfb=%{time_starttransfer}s total=%{time_total}s http_code=%{http_code}\n' -H 'Host: <domain>' http://<jail-ip>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Hit the jail's own IP directly with the right `Host:` header (per the
|
||||||
|
`bastille-jail-provisioning` skill's testing gotcha) unless the site's
|
||||||
|
public vhost is already fully wired — hitting the host's `127.0.0.1` with
|
||||||
|
a `Host:` header before that vhost exists silently falls through to a
|
||||||
|
different site's `server_name` and produces a misleadingly "fast" TTFB
|
||||||
|
for the wrong page entirely. Take at least 3 samples and use the median;
|
||||||
|
a single run is noise.
|
||||||
|
|
||||||
|
**2. Autoloaded-options size:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@<host> "bastille cmd <jail> sh -c 'cd <site-path> && su -m www -c \"wp option list --autoload=on --format=total_bytes --path=<site-path>\"'"
|
||||||
|
```
|
||||||
|
|
||||||
|
No fixed "too big" threshold is asserted here — compare against the
|
||||||
|
site's own history or a healthy sibling jail rather than trusting one
|
||||||
|
specific byte count as universally bad. To find the biggest individual
|
||||||
|
offenders, add `--fields=option_name,size_bytes` and sort the output.
|
||||||
|
|
||||||
|
**3. Object-cache presence:**
|
||||||
|
|
||||||
|
Don't assume every site has a persistent object cache. As of the most
|
||||||
|
recent audit (`docs/server-granja.md`, `docs/server-cerebro.md`), only
|
||||||
|
`menorca.pt`/`menorca.ro` on granja have the Redis Object Cache plugin
|
||||||
|
wired against cerebro's Valkey instance — the other granja jails, and
|
||||||
|
Cala Verd's MariaDB-only setup, do not. Check, don't assume:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
... wp eval 'echo wp_using_ext_object_cache() ? "yes" : "no";' --path=<site-path>
|
||||||
|
```
|
||||||
|
|
||||||
|
plus confirm the drop-in file exists: `bastille cmd <jail> test -f
|
||||||
|
<site-path>/wp-content/object-cache.php && echo present`. If present, `wp
|
||||||
|
redis status` (Redis Object Cache plugin) reports connection health —
|
||||||
|
Valkey lives on `cerebro`, with per-site ACL credentials; see the
|
||||||
|
Redaction section below before pasting any of that output verbatim
|
||||||
|
anywhere.
|
||||||
|
|
||||||
|
**4. Cron health:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
... wp cron event list --path=<site-path>
|
||||||
|
... wp cron test --path=<site-path>
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for: events stuck far in the past (missed spawns), duplicate events
|
||||||
|
on the same hook, and any heavy-looking hook (import/sync/content-agent
|
||||||
|
jobs — several granja sites are fed by hourly RSS/content-agent crons per
|
||||||
|
`docs/server-granja.md`) that might be running inline on a page load
|
||||||
|
instead of via a real WP-Cron spawn.
|
||||||
|
|
||||||
|
## Step 4 — deeper diagnostics (only if the four baseline metrics don't explain it)
|
||||||
|
|
||||||
|
Load [`references/diagnostic-tools.md`](references/diagnostic-tools.md)
|
||||||
|
for `wp doctor`, `wp profile stage/hook/eval`, and headless Query Monitor
|
||||||
|
usage via REST response headers — all still read-only, still WP-CLI/curl
|
||||||
|
based, no browser required.
|
||||||
|
|
||||||
|
Load [`references/fix-patterns.md`](references/fix-patterns.md) once a
|
||||||
|
dominant bottleneck category is identified (database/queries, autoload,
|
||||||
|
object cache, cron, remote HTTP calls) — condensed fix-pattern guidance
|
||||||
|
per category, still gated by the same read-only-first / measure-before-
|
||||||
|
and-after discipline as everything above.
|
||||||
|
|
||||||
|
## Redaction — before pasting ANY of this into a report
|
||||||
|
|
||||||
|
Diagnostic output on this fleet can surface real secrets. Before quoting
|
||||||
|
command output verbatim in a report, chat message, or committed doc,
|
||||||
|
redact:
|
||||||
|
|
||||||
|
- `wp config list` / raw `wp-config.php` contents — `DB_PASSWORD`, auth
|
||||||
|
keys/salts, `WP_REDIS_USERNAME`/`WP_REDIS_PASSWORD` (Valkey ACL
|
||||||
|
credentials; per `docs/server-cerebro.md`, per-site passwords live at
|
||||||
|
`/root/.granja_valkey_creds` on cerebro, the admin password at
|
||||||
|
`/root/.valkey_admin_pw` — never paste these values themselves either).
|
||||||
|
- `wp option list` output that happens to include API keys stored as
|
||||||
|
plugin options (payment-gateway keys, mail-relay credentials, etc.).
|
||||||
|
- Any Application Password generated while setting up headless Query
|
||||||
|
Monitor REST auth (see `references/diagnostic-tools.md`).
|
||||||
|
- Full stack traces / DB error output that can echo a connection string.
|
||||||
|
|
||||||
|
Follow the `<REDACTED>` convention used elsewhere in this library (see
|
||||||
|
the `diagnosing-bugs` skill): replace the secret value in place, keep
|
||||||
|
everything else so the diagnostic signal survives. When in doubt whether
|
||||||
|
a value is sensitive, redact it — the cost of over-redacting is a
|
||||||
|
follow-up question; the cost of under-redacting is a leaked credential in
|
||||||
|
a transcript or a committed doc.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Baseline and after-fix numbers were captured with the *same* method
|
||||||
|
(Step 3, same site, same `--path`/`--url`, ideally the same time of
|
||||||
|
day — don't compare a cold-cache run to a warm one).
|
||||||
|
- `wp cron test` / `wp doctor check` (if installed) are clean or
|
||||||
|
improved.
|
||||||
|
- No new PHP errors in the jail's PHP-FPM/WordPress debug log after the
|
||||||
|
change.
|
||||||
|
- If the fix required a cache flush, that flush is the *last* step
|
||||||
|
taken, not a diagnostic shortcut — flushing before measuring erases the
|
||||||
|
baseline you needed to compare against.
|
||||||
|
|
||||||
|
## Failure modes
|
||||||
|
|
||||||
|
- **"No change" after a fix** — check you're hitting the same jail IP /
|
||||||
|
domain (`Host:` header) as the baseline, and that PHP's opcache isn't
|
||||||
|
serving a stale compiled version (clearing it, or restarting php-fpm,
|
||||||
|
is itself a diagnostic action worth noting in the report, not a routine
|
||||||
|
step to take silently).
|
||||||
|
- **Noisy TTFB/profiling numbers** — an hourly content-agent cron job on
|
||||||
|
granja sites, or a plugin update check, landing mid-sample can skew a
|
||||||
|
single `curl` run. Take multiple samples.
|
||||||
|
- **Object-cache check says "no" but you expected "yes"** — confirm which
|
||||||
|
site this actually is; only two of granja's 13 jails currently have
|
||||||
|
Redis Object Cache wired up. Check the inventory before treating that
|
||||||
|
as a bug.
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
- Do not install plugins (including the `wp doctor`/`wp profile`
|
||||||
|
packages themselves), enable `SAVEQUERIES`/`WP_DEBUG` in a way that
|
||||||
|
could linger, or flush any shared cache without explicit approval —
|
||||||
|
cerebro's Valkey DB 0 is genuinely shared across sites, and `wp redis
|
||||||
|
enable`'s `FLUSHDB` call can transiently clear a *different* site's
|
||||||
|
cache too (confirmed in `docs/server-cerebro.md`). Treat this as
|
||||||
|
production-adjacent even on a "staging" host — the `staging` host has
|
||||||
|
hosted real production traffic before (see `docs/server-staging.md`).
|
||||||
|
- If system-level profiling (an APM, a PHP profiler extension) is needed
|
||||||
|
beyond WP-CLI's own `profile` package, that's an infra change — flag it
|
||||||
|
rather than installing it unilaterally on a jail.
|
||||||
|
|
||||||
|
## Version note
|
||||||
|
|
||||||
|
Do not assume a fleet-wide WordPress version. The upstream `wp-performance`
|
||||||
|
skill asserts "WordPress 7.0+, PHP 7.4.0+" as its own compatibility
|
||||||
|
target — treat that as the upstream author's target, not a verified fact
|
||||||
|
about this fleet. Confirm per-site with `wp core version --path=<site-path>`.
|
||||||
|
As one dated data point: granja's 13 WordPress jails were confirmed on
|
||||||
|
core 7.0.4 during the 2026-08-12 fleet-wide security rollout
|
||||||
|
(`docs/server-granja.md`) — that's a specific fact about one host at one
|
||||||
|
point in time, not a floor to assume for staging, gringo, or any future
|
||||||
|
site.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Deeper diagnostic tools (still read-only, still backend-only)
|
||||||
|
|
||||||
|
Load this file only once the four core baseline metrics (TTFB, autoload
|
||||||
|
size, object-cache presence, cron health — see `SKILL.md` Step 3) haven't
|
||||||
|
explained the symptom. Every command here still goes through the fleet's
|
||||||
|
`bastille cmd ... su -m www -c "wp ..."` wrapper from `SKILL.md` Step 2 —
|
||||||
|
abbreviated to bare `wp ...` below for readability.
|
||||||
|
|
||||||
|
## `wp doctor` — quick production-readiness checks
|
||||||
|
|
||||||
|
Catches common footguns fast: autoload bloat, `SAVEQUERIES`/`WP_DEBUG`
|
||||||
|
left on, stale plugin/core versions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp doctor check
|
||||||
|
wp doctor list # see available checks
|
||||||
|
```
|
||||||
|
|
||||||
|
Install if missing (this is a write action — confirm it's acceptable
|
||||||
|
before running it, per Step 0):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp package install wp-cli/doctor-command
|
||||||
|
```
|
||||||
|
|
||||||
|
Checks especially relevant to performance:
|
||||||
|
|
||||||
|
- `autoload-options-size` — autoloaded-options threshold
|
||||||
|
- `constant-savequeries-falsy` / `constant-wp-debug-falsy` — flags
|
||||||
|
perf-costly debug constants left on in what should be production
|
||||||
|
- cron checks (event count / duplicates)
|
||||||
|
|
||||||
|
Docs: default checks —
|
||||||
|
https://make.wordpress.org/cli/handbook/doctor-default-checks/ ·
|
||||||
|
customizing checks —
|
||||||
|
https://make.wordpress.org/cli/handbook/guides/doctor/doctor-customize-config/
|
||||||
|
|
||||||
|
## `wp profile` — hook/stage-level profiling without a browser
|
||||||
|
|
||||||
|
Install if missing (write action, confirm first):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp package install wp-cli/profile-command
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended sequence:
|
||||||
|
|
||||||
|
1. **Stage overview** — where time goes across bootstrap/main_query/template:
|
||||||
|
```bash
|
||||||
|
wp profile stage --fields=stage,time,cache_ratio [--url=<url>]
|
||||||
|
```
|
||||||
|
2. **Hook hotspots**:
|
||||||
|
```bash
|
||||||
|
wp profile hook --spotlight [--url=<url>]
|
||||||
|
wp profile hook init --spotlight [--url=<url>] # drill into one hook
|
||||||
|
```
|
||||||
|
3. **Targeted eval**:
|
||||||
|
```bash
|
||||||
|
wp profile eval 'do_action("init");' --hook=init
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--url=` to profile a specific site/route on a multisite-adjacent
|
||||||
|
setup. `--skip-plugins`/`--skip-themes` can isolate a culprit component,
|
||||||
|
but changes real behavior while active — note that in the report if you
|
||||||
|
use it, don't silently profile a degraded version of the site.
|
||||||
|
|
||||||
|
Docs: https://wpcli.dev/docs/profile/stage ·
|
||||||
|
https://wpcli.dev/docs/profile/hook ·
|
||||||
|
https://wpcli.dev/docs/profile/eval
|
||||||
|
|
||||||
|
## Query Monitor, used headlessly
|
||||||
|
|
||||||
|
Query Monitor is normally UI-driven, but it exposes data through
|
||||||
|
authenticated REST response headers/envelope — no browser required.
|
||||||
|
|
||||||
|
1. Confirm the plugin is active: `bastille cmd <jail> test -f
|
||||||
|
<site-path>/wp-content/plugins/query-monitor/query-monitor.php`.
|
||||||
|
2. Authenticate against the REST API — a nonce, or an Application
|
||||||
|
Password (see Redaction in `SKILL.md`: never paste a generated
|
||||||
|
Application Password into a report).
|
||||||
|
3. Request a REST route and inspect response headers (`x-qm-overview-*`
|
||||||
|
etc.), or request an enveloped response (`?_envelope`) to get a `qm`
|
||||||
|
property containing DB query details, cache stats, and HTTP API call
|
||||||
|
details in one payload.
|
||||||
|
|
||||||
|
Configuration constants: https://querymonitor.com/help/configuration-constants/ ·
|
||||||
|
REST-API-specific docs: https://querymonitor.com/wordpress-debugging/rest-api-requests/ ·
|
||||||
|
plugin page: https://wordpress.org/plugins/query-monitor/
|
||||||
|
|
||||||
|
Guardrails: Query Monitor adds real overhead — don't enable it in
|
||||||
|
production without approval. If a hosting platform pre-installs it
|
||||||
|
gated behind a capability, you may need `view_query_monitor` granted to
|
||||||
|
the authenticating user.
|
||||||
|
|
||||||
|
## Server-Timing headers (if Performance Lab is present)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -D - -H 'Host: <domain>' http://<jail-ip>/ -o /dev/null | grep -i '^server-timing:'
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires the Performance Lab plugin (or a module of it) enabled —
|
||||||
|
https://wordpress.org/plugins/performance-lab/. Don't enable experimental
|
||||||
|
modules in production without approval; this is a nice-to-have on top of
|
||||||
|
the four core baseline metrics, not a replacement for them.
|
||||||
|
|
||||||
|
Benchmarking guidance: https://make.wordpress.org/performance/handbook/measuring-performance/benchmarking-server-timing/
|
||||||
|
|
||||||
|
## General measurement discipline
|
||||||
|
|
||||||
|
- Always capture a baseline before changing anything.
|
||||||
|
- Keep the test scenario fixed — same URL/route, same logged-in-or-not
|
||||||
|
state, same underlying data.
|
||||||
|
- Prefer multiple samples and a median over a single run.
|
||||||
|
|
||||||
|
Measuring-performance handbook:
|
||||||
|
https://make.wordpress.org/performance/handbook/measuring-performance/
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# Fix patterns by bottleneck category
|
||||||
|
|
||||||
|
Load this file once `references/diagnostic-tools.md` (or the Step 3
|
||||||
|
baseline metrics) has pointed at a dominant bottleneck. Pick *one*
|
||||||
|
primary category to fix at a time — trying to fix everything in one pass
|
||||||
|
makes the before/after Verification step in `SKILL.md` meaningless, since
|
||||||
|
you won't know which change caused which delta.
|
||||||
|
|
||||||
|
Every fix below is still gated by `SKILL.md`'s Step 0 (read-only-first)
|
||||||
|
and Verification (re-measure the same way afterward) sections.
|
||||||
|
|
||||||
|
## Database / query performance
|
||||||
|
|
||||||
|
Use when profiling points at DB time or a high query count.
|
||||||
|
|
||||||
|
- Avoid N+1 query patterns — batch queries, prime caches, avoid per-row
|
||||||
|
lookups inside a loop.
|
||||||
|
- Prefer `fields => 'ids'` in `WP_Query`/`get_posts()` when only IDs are
|
||||||
|
needed.
|
||||||
|
- Avoid expensive meta queries where possible; consider indexing or a
|
||||||
|
schema change for genuinely hot lookups.
|
||||||
|
- Use object caching for repeated reads (see below) rather than
|
||||||
|
re-querying the same data every request.
|
||||||
|
|
||||||
|
Backend-only tools: Query Monitor via REST (query lists, stack traces,
|
||||||
|
slow/duplicate flags) — see `diagnostic-tools.md`. `wp db query` for
|
||||||
|
targeted SQL/`EXPLAIN` — be careful running this in production, it's a
|
||||||
|
direct DB access path.
|
||||||
|
|
||||||
|
Query Monitor plugin: https://wordpress.org/plugins/query-monitor/
|
||||||
|
|
||||||
|
## Autoloaded options
|
||||||
|
|
||||||
|
Autoloaded options load on *every* request, so a large autoload payload
|
||||||
|
hurts every page, not just the page that set the option.
|
||||||
|
|
||||||
|
Quick checks (see `SKILL.md` Step 3 for the always-collect total):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp option list --autoload=on --fields=option_name,size_bytes | sort -n -k 2 | tail
|
||||||
|
```
|
||||||
|
|
||||||
|
Fix patterns:
|
||||||
|
|
||||||
|
- Stop autoloading large blobs — store large/rarely-read data with
|
||||||
|
`autoload=off` instead.
|
||||||
|
- Move large computed data to transients or the persistent object cache
|
||||||
|
(if the site has one — check first, see `SKILL.md` Step 3 metric 3)
|
||||||
|
rather than an autoloaded option.
|
||||||
|
- Remove stale options left behind by removed plugins/themes — confirm
|
||||||
|
nothing still reads the option before deleting; a stale-looking option
|
||||||
|
name isn't proof nothing depends on it.
|
||||||
|
|
||||||
|
Docs: `wp option list` — https://wpcli.dev/docs/option/list · `wp doctor`'s
|
||||||
|
`autoload-options-size` check —
|
||||||
|
https://make.wordpress.org/cli/handbook/doctor-default-checks/
|
||||||
|
|
||||||
|
## Object caching
|
||||||
|
|
||||||
|
Use when profiling shows repeated identical queries or a low cache hit
|
||||||
|
rate.
|
||||||
|
|
||||||
|
- WordPress's default object cache is per-request memory only — it does
|
||||||
|
not persist across requests unless a persistent drop-in
|
||||||
|
(`wp-content/object-cache.php`) is present. On this fleet, that's
|
||||||
|
currently only true for `menorca.pt`/`menorca.ro` (Redis Object Cache
|
||||||
|
plugin against cerebro's Valkey) — see `SKILL.md` Step 3 metric 3
|
||||||
|
before assuming a site has one.
|
||||||
|
- `wp cache flush` (or the Redis-plugin-specific `wp redis flush`) can
|
||||||
|
affect more than the one site you're working on — cerebro's Valkey DB 0
|
||||||
|
is genuinely shared across sites by key-prefix, not fully isolated per
|
||||||
|
site (confirmed in `docs/server-cerebro.md`: `wp redis enable`'s
|
||||||
|
`FLUSHDB` clears the whole shared DB 0, not just the calling site's
|
||||||
|
prefix). Never flush without explicit approval.
|
||||||
|
|
||||||
|
Fix patterns:
|
||||||
|
|
||||||
|
- Cache expensive computed results (transients, or the persistent object
|
||||||
|
cache where one exists) with explicit invalidation — don't rely on
|
||||||
|
natural expiry alone for data that changes on a known event.
|
||||||
|
- Avoid unbounded caches — set expirations, or implement real
|
||||||
|
invalidation hooks tied to the data changing.
|
||||||
|
- If a site doesn't have a persistent object cache and profiling
|
||||||
|
genuinely points there, adding one is an infra change (provisioning
|
||||||
|
Valkey ACL credentials on cerebro, installing the PHP Redis extension
|
||||||
|
in the jail — package name `php85-pecl-redis`, not `php85-redis`, per
|
||||||
|
`bastille-jail-provisioning`) — coordinate rather than doing it as a
|
||||||
|
drive-by inside a performance investigation.
|
||||||
|
|
||||||
|
WP-CLI cache commands: https://wpcli.dev/docs/cache · flush guardrails:
|
||||||
|
https://wpcli.dev/docs/cache/flush
|
||||||
|
|
||||||
|
## Cron
|
||||||
|
|
||||||
|
Use when cron causes request-time spikes or slowness.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp cron event list
|
||||||
|
wp cron test # spawning health
|
||||||
|
wp cron event run --due-now
|
||||||
|
```
|
||||||
|
|
||||||
|
Fix patterns:
|
||||||
|
|
||||||
|
- De-duplicate scheduled events and reduce frequency where the task
|
||||||
|
genuinely doesn't need to run that often.
|
||||||
|
- Ensure cron callbacks are idempotent and short — a task that's slow
|
||||||
|
enough to matter for site performance is a candidate for moving off the
|
||||||
|
request path entirely, not just running it less often.
|
||||||
|
- Move heavy work (imports, RSS/content-agent jobs — several granja sites
|
||||||
|
already run these hourly, per `docs/server-granja.md`) fully off the
|
||||||
|
page-load request path; a badly-timed spawn on a real WP-Cron pseudo-cron
|
||||||
|
setup can add real latency to whichever request happens to trigger it.
|
||||||
|
|
||||||
|
WP-CLI cron command package: https://github.com/wp-cli/cron-command
|
||||||
|
|
||||||
|
## Remote HTTP API calls
|
||||||
|
|
||||||
|
Use when profiling shows slow external requests (`wp_remote_get()` etc.).
|
||||||
|
|
||||||
|
Fix patterns:
|
||||||
|
|
||||||
|
- Add explicit timeouts and fail-fast behavior — a slow or hanging
|
||||||
|
third-party API shouldn't be able to make every page load slow.
|
||||||
|
- Cache responses where the data doesn't need to be live on every request
|
||||||
|
(transients / persistent object cache where available).
|
||||||
|
- Batch requests, and avoid calling a remote API on every page load when
|
||||||
|
a periodic cron-driven refresh would do.
|
||||||
|
- Move genuinely heavy remote work off the request path entirely (cron or
|
||||||
|
a queue), same principle as the Cron section above.
|
||||||
|
|
||||||
|
Query Monitor can report HTTP API call timing via the REST envelope
|
||||||
|
(`qm` property) — see `diagnostic-tools.md`.
|
||||||
Reference in New Issue
Block a user