| name | magento2-performance-audit |
| description | This skill should be used when the user asks to "audit performance", "check Core Web
Vitals", "run Lighthouse", "check server configuration", "verify Redis/Varnish setup",
"analyze database queries", "find N+1 query issues", "review indexer configuration", "check
cron health", "debug cache flush", asks "why does full_page cache keep flushing", wants to
"trace FPC invalidation", or reports "too many ajax requests", a "customer data section
reload storm", or a "crawler overloading server". Performs a comprehensive performance and
health audit for Magento 2 projects against Adobe Commerce Best Practices. DEPENDENT on
magento2-dev-core for code-level performance patterns.
|
| compatibility | claude, codex, opencode, copilot, dsh |
| depends | ["magento2-dev-core"] |
| metadata | {"audience":"developers","workflow":"magento"} |
Magento 2 Performance Audit
This skill performs a comprehensive audit of Magento 2 performance, infrastructure, and code-level patterns.
Govard-Native Audit Coverage
govard audit run executes PHPCS and PHPStan through Govard's pinned lint
toolchain image — it covers coding-standard and static-analysis findings only.
As of Govard v1.64.0 no performance audit check exists:
govard audit run --checks performance fails with "audit check ... is not
implemented". Govard v1.64.0 does add a native profiler check
(--checks lint,profiler --url <url>) that machine-captures the stock profiler CSV for one
URL — a quick complement to this skill's manual per-page audit, not a replacement: it ships
no query log, no cross-page matrix, and no threshold analysis. The profiler requires a project target (not standalone), an absolute http(s) URL (the request carries Accept: text/html so stock Magento enables the CSV), and is guarded by a per-project diagnostics lease; the CSV lands as artifacts/profiler/profile.csv with its SHA in audit-result.json — open it as spreadsheet to read per-timer costs. Manual per-page captures (7 pages) cost ~2.5-3 min on reference project (50k queries/page with call-stack, 16-26s each) — keep all 7, run with 300s timeout or background polling and trap restore, not by sampling fewer categories (that hides per-item N+1s). Keep running this checklist
yourself and treat govard audit run --checks lint as the shared lint gate. Never present a
lint-only pass as a performance verdict.
This is a checklist, not a menu. All 9 steps under Workflow (bottom of this file) run on every invocation — infra, indexer/cron, per-page-type capture, Slow Query Analysis, Cache Invalidation Efficiency, Client-Side AJAX Load, Core Web Vitals, code-level grep, report. Picking the steps that feel highest-signal for the effort and quietly dropping the rest (no admin creds, no Chrome DevTools MCP, "I already found a good bug") is the single most common failure mode of this skill — it produces a confident, well-formatted report that silently covers less than half the checklist. If a step genuinely can't run, say so in the report, under that step's own heading — Skipped: <reason> — never by omission. See the self-verification gate at the end of Workflow: the report is not done until it's been checked against the Audit Report Template line by line.
Distinguish a scoped ask from an unscoped one — this rule governs dropping steps quietly, not answering a narrower question. A general ask — "audit performance", "review this project before launch" — is unscoped: all 9 steps apply, none optional, exactly as above. When the user's own words name one specific category instead ("just check the MySQL query count", "audit N+1s only", "how many queries does the homepage run"), scope the work to that category and its reference file(s) — running the other 8 steps anyway would be answering a different question than the one asked. The obligation that carries over unchanged: state the scope explicitly (a "Scope" line/heading in the report) so the result is never mistaken for a full audit, and don't let scope creep run in either direction — no silently expanding a scoped ask back to all 9 steps, and no silently narrowing an unscoped one down to whichever step already found something.
Read the step's reference file before the first attempt at that step — not after improvising fails. Each "Full detail:" link owns its supported commands and environment traps: BusyBox grep inside govard sh, host/container DNS, Laminas renames, and DB privilege walls. If two attempts at a step fail, read that reference end to end before attempt 3. Never hand-edit app/etc/env.php to enable a diagnostic: it is deployment configuration, not an audit control surface; a syntax error takes every Magento CLI command down and cannot be reverted with git checkout when the file is gitignored.
Shared query-log captures require ownership. Before step 3, acquire the .performance-audit.lock session token described in references/database-query-profiling.md. A busy or foreign lock must fail-fast; do not remove a lock you do not own or treat its log as evidence.
Common ways this gets shortcut (don't)
| Rationalization | Reality |
|---|
| "Infra/cache/indexer checks already give strong signal, that's enough" | Slow Query Analysis, Cache Invalidation Efficiency, and Client-Side AJAX Load each catch bug classes the others structurally cannot see — one being clean says nothing about the others |
| "I already found a solid N+1, that's enough for a report" | A real finding proves the audit found something; it doesn't prove the mandatory steps ran. Finding a bug early is not a reason to stop the checklist |
| "One category and one product page is representative enough" | Only 3 differently-sized samples per type can surface the size-scaling N+1 signal (references/per-page-type-audit.md) — a single sample is provably unable to show it, however clean the one page looks |
| "This step needs admin credentials / a Chrome DevTools MCP I don't have" | Mark that section unverified with the reason, in the report — don't drop it from the conversation as if it were never in scope |
| "The draft report already has good findings, ship it" | Diff the draft against every checkbox in the Audit Report Template before presenting it as done — an unchecked box with no skip reason means the audit isn't finished, not that it's fine to omit |
| "This query shape (or profiler timer) repeats/is slow but I don't think it's a real bug" | Not your call to make silently — list it in the Repeated Query Shapes or Slowest Blocks/Templates table (references/report-template.md) with your assessment anyway. A borderline case left out of the report is indistinguishable from one that was never checked |
| "The user only asked about query counts, so I only ran that" | Correct if their own words named that one category — say so under a Scope heading. If their ask was general ("audit performance", "review this project"), this is the same shortcut as the rows above, just dressed up as scoping |
"There's no dev:profiler:enable / dev:query-log:enable flag, so I'll add one to env.php / patch a bootstrap script" | These are CLI diagnostics, not config flags. Use the exact commands in references/database-query-profiling.md and references/per-page-type-audit.md; never edit env.php or add a one-off bootstrap script |
"The reference's grep recipes don't work in govard sh, so I'll write my own" |
Related Skills
REQUIRED BACKGROUND: Load magento2-dev-core first — code-level fixes for N+1 queries and heavy constructors follow the patterns it defines.
Part of the QA trio with magento2-linter and magento2-security-scan — together with magento2-dev-core, these form the "QA quartet" that magento2-code-review orchestrates at PR/module/theme/project scope. Findings use the shared M2-PERF-xxx codes cataloged in magento2-dev-core/references/severity-and-codes.md. Async/queue findings often point back to magento2-backend-dev.
Audit Categories
Nine categories, each with full commands/thresholds/edge-cases in its own reference file — read the relevant file when executing that step of the Workflow below, not all up front:
| Category | Reference |
|---|
| Infrastructure, cache, indexer, async consumers, asset optimization, cron, security probes | references/infrastructure-checks.md |
| Core Web Vitals (LCP/INP/CLS, Chrome DevTools MCP trace, Lighthouse fallback) | references/core-web-vitals.md |
| Database query profiling: query-count tiers, query log setup, common issues, Slow Query Analysis | references/database-query-profiling.md |
| HTML profiler: block/template timing, tracing custom-code cost, cross-page-type signals | references/html-profiler-audit.md |
| Per-page-type audit (homepage + 3 category + 3 product samples, uncached) | references/per-page-type-audit.md |
| Cache invalidation efficiency (built-in FPC debug log + Varnish BAN tracing) | references/cache-invalidation-audit.md |
| Client-side AJAX/Customer Data load (sections.xml, reload storms) | references/ajax-load-audit.md |
| Code-level performance patterns (N+1, collection counting, heavy constructors, cache invalidation code) | references/code-level-patterns.md |
| Audit report template + self-verification checklist | references/report-template.md |
Quick / Deep — scope param (quick vs deep, quick 3–5m vs deep 8–12m)
This skill accepts a scope param: quick (PR check, cap 3–5m) or deep (release audit, 8–12m). The value quick.*deep on one line is intentional for tooling checks — keep the param name scope with those two literal values. Default to deep when the caller does not specify; callers that need a fast PR signal pass scope=quick.
Report header (mandatory): every report starts with Scope: quick or Scope: deep on its first line (see references/report-template.md). Quick uses Scope: quick — 3 pages (1 home + 1 category + 1 product), Deep uses Scope: deep — 7 pages (1 home + 3 category small/medium/large + 3 product). Do not start a report without that line — it is how a reader tells PR vs release coverage at a glance.
| Mode | Pages | Query-log call-stack | Query-time threshold | Files scope | grid_per_page | Time cap | Transport & restore |
|---|
| quick | 3 pages small/medium/large (1 home + 1 category + 1 product) — call-stack false — threshold 1 — quick files — grid_per_page 48 — cap 3–5m | --include-call-stack=false | --query-time-threshold=1 | quick files (app/code + app/design + app/etc sampled, batch govard sh) | 48 | 3–5m | On DSH: call maestro_perf_log_stats streaming (bounded 2 MiB, server-side cat var/debug/db.log) / Otherwise: grep; batch govard sh single setup cmd; trap single |
| deep | 7 pages double-pass (1 home + 3 category small/medium/large + 3 product) — call-stack true — threshold 0 — deep files — 8–12m | --include-call-stack=true (two-pass: pass 1 false for counts, pass 2 true for 1–2 traces) | --query-time-threshold=0 | deep files (full app/code + vendor/dev/lib/m2-hotfixes to the ignore boundary) | prod grid_per_page/list_per_page | 8–12m | Same On DSH/Otherwise split; batch govard sh where possible; trap single (one trap ... EXIT for the whole session) |
Key verbatim mapping for quick 3 pages small/medium/large call-stack false threshold 1 quick files grid_per_page 48 cap 3–5m vs deep 7 pages double-pass call-stack true threshold 0 deep files 8–12m — keep these values in sync with references/per-page-type-audit.md and references/database-query-profiling.md.
DSH prefers tools — keep skills vs plugins separate (A): On DSH: call maestro_perf_log_stats (tool) for query-log stats; do not hand-grep 50k lines or open var/debug/db.log as spreadsheet. Otherwise: use the grep -c '## QUERY' / mysqldumpslow / pt-query-digest recipes in references/database-query-profiling.md. Same split for lint: On DSH govard_audit_lint --scope diff --base origin/master (quick) vs --scope project (deep); Otherwise govard audit run --checks lint --format json on the host.
On DSH: call maestro_perf_log_stats / Otherwise: grep — DSH prefers tools (skills vs plugins separate).
Batch govard sh + trap single: collapse multi-step container setup (mkdir .performance-audit.lock, dev:profiler:enable, dev:query-log:enable, cache:disable, cache:flush, warmup) into one govard sh -c "..." round-trip where sequencing allows; captures themselves stay sequential under the same lock. Always install a single trap 'govard sh -c "bin/magento cache:enable ... && bin/magento cache:flush && bin/magento dev:profiler:disable && bin/magento dev:query-log:disable && rm -rf var/debug/.performance-audit.lock"' EXIT — one trap for the entire audit, not per page — so a timeout restores caches/log/lock.
Workflow
On DSH: call maestro_perf_log_stats {topN?,repeatThreshold?,timeThresholdMs?} → {slowQueries,nPlusOneCandidates,cacheFlushStorm,crawlerOverload}. Do not hand-grep 16–50k lines or open as spreadsheet.
Otherwise: grep -c '## QUERY' / mysqldumpslow recipes in references/database-query-profiling.md.
On DSH: no performance check exists — govard audit run --checks lint covers lint only; use govard audit run --checks profiler --url <absolute http(s) url> for a single-URL profiler CSV where it helps, otherwise run the manual per-page captures and grep scans below.
Otherwise: run the same manual Workflow steps as written (profiler + query-log captures per references/per-page-type-audit.md and grep scans per references/code-level-patterns.md).
Step 0 — Branch & Env Gate (tool > LLM, interactive when On DSH): before infra, ensure the requested git branch is checked out and the Govard environment is ready. Tool magento2-performance-audit({branch?:string, scope?:'quick'|'deep'}) — branch optional. When branch is provided: git fetch --all --prune → git rev-parse --verify <branch> → git checkout -f <branch> if different → git pull --ff-only origin/<branch> (if diverged → Skipped: local diverged — audit on current HEAD). When branch is undefined and On DSH: ask via ask_user_question (1) current checked-out (2) input branch (3) choose from git branch -a then same fetch/checkout/pull. After branch: govard status → if Down or project mismatch → govard env up (wait Ready php runtime ~11s like example-project) + curl -skI https://<domain>/ 200 verify before step 1.
Step 0b — Discovery host-first (5s max): see references/per-page-type-audit.md §0 — DB candidates via <prefix>url_rewrite, host-first curl --max-time 5 --connect-timeout 3 (On DSH host curl / Otherwise container curl), prefix from env.php/SHOW TABLES, is_active via information_schema (not hard-coded), fallback Skipped: no 200 URL. Quick ≤15s (1 cat+1 product), deep ≤30s (3+3); quick 3 pages vs deep 7 pages preserved, 7 details gate unchanged.
When invoked, steps 1-9 are mandatory, run in order, none optional:
- Execute infrastructure checks (env.php, mode, cache status) — first confirm whether the target is local dev, staging, or production, since expectations differ. Full detail:
references/infrastructure-checks.md
- Run indexer status check, and verify cron is actually running/draining
cron_schedule. Full detail: references/infrastructure-checks.md
- Run the Per-Page-Type Audit — homepage plus 3 category (small/medium/large) and 3 product URLs — with
full_page/block_html/layout caches disabled — verify each test page is representative first, then capture profiler + query log together, watch for a query count (and a slow block/template timer) that scales with grid size across the 3 category samples, then restore state. Use the setup, capture, restoration, and host-side request commands owned by the linked references.
Full detail: references/per-page-type-audit.md, references/database-query-profiling.md, and references/html-profiler-audit.md
- Run Slow Query Analysis (app-level
TIME: sort, and/or MySQL's own slow_query_log for cron/import-triggered queries the app-level log can't see) — EXPLAIN any candidate before reporting it, and turn slow_query_log back off when done. Full detail: references/database-query-profiling.md
- Trace cache invalidation efficiency — enable temporary logging (debug.log for built-in Redis/file FPC, varnishlog/ban.list for Varnish), reproduce one isolated save/action (or mark unverified if no admin credentials are available this session), and flag any custom code causing broad/frequent flushes beyond Magento's default targeted invalidation. Full detail:
references/cache-invalidation-audit.md
- Confirm which reactive/AJAX mechanism the project actually uses (sections.xml/Customer Data vs. Magewire/PWA/GraphQL or similar), then capture the AJAX footprint of a fresh/anonymous page load (Network tab) and audit accordingly — for sections.xml, check for overly broad Customer Data invalidation rules; these uncacheable requests are what crawler/bot JS execution multiplies regardless of FPC hit rate. Full detail:
references/ajax-load-audit.md
- Run Core Web Vitals audit (Chrome DevTools MCP trace preferred, Lighthouse as fallback) — when render delay dominates an LCP, read it per the JS-hydration guidance rather than assuming a network/image problem. Full detail:
references/core-web-vitals.md
- Scan
app/code for code-level performance patterns using the grep recipes. Full detail: references/code-level-patterns.md
10. Self-verification gate — mandatory, run before presenting the report to the user:
Pacing: do not inflate setup. Run the reference-owned setup commands one at a time, then move directly into captures. If setup for a step takes more than a couple of calls, stop and read that step's reference file instead of iterating through unsupported variants.
Walk the draft report against every checkbox in the Audit Report Template (references/report-template.md), one by one. For each checkbox, exactly one of these must be true:
- It's checked, with evidence for it visible somewhere above in the report (a command output, a query-log count, a traced file:line).
- It's unchecked, with an explicit
Skipped: <reason> line next to it (missing credentials, no Chrome DevTools MCP, environment doesn't apply).
A checkbox that is simply absent from the report — not checked, not marked skipped, just not mentioned — means step 10 hasn't been done yet. Go back and either run the missing step or add the skip reason; don't publish or hand off the report in that state. Only once every checkbox resolves to one of the two states above is the audit actually finished — both report.md and report.html (identical content, see step 9) must exist and have been checked against the template; publish report.html only if the user chose to in step 9 (otherwise keep both locally).