magento2-performance-audit · git:20260827.4bf4e2d · 2026-08-27 · sha256 2130aa4cb7bfbf9f

magento2-performance-audit git:20260827.4bf4e2dA

Immutable. This exact content is served forever at /api/v1/blob/2130aa4cb7bfbf9f.

---
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" | The container's grep is BusyBox — no `--include`. `find app/code -name '*.php' -exec grep -Hn ... {} +` is the supported equivalent (recipes in `references/code-level-patterns.md` are already BusyBox-safe) |

## 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`).

**When invoked, steps 1-9 are mandatory, run in order, none optional:**
1. 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`
2. Run indexer status check, and verify cron is actually running/draining `cron_schedule`. Full detail: `references/infrastructure-checks.md`
3. 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`
4. 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`
5. 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`
6. 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`
7. 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`
8. Scan `app/code` for code-level performance patterns using the grep recipes. Full detail: `references/code-level-patterns.md`
9. Draft the report with recommendations, prioritizing any finding that repeats across all 3 page types (site-wide impact) over page-specific ones — and populate the Repeated Query Shapes table (every shape repeated more than ~5 times per page load), the **Per-Page Query Detail** breakdown (the complete, unfiltered per-page query list — **mandatory for deep**: 7 tables one per page via `maestro_perf_log_stats` streaming, see gate in `references/report-template.md`; quick: `Skipped: quick — 3 pages only, Per-Page Detail deferred to deep`), and the Slowest Blocks/Templates table (every profiler timer past the threshold in `references/html-profiler-audit.md`), not just the ones already confirmed as bugs. **Always produce both** `report.md` (via `references/report-template.md`) and `report.html` (via `references/report.html.template` + inline CSS from `references/report-theme.md` — copy verbatim, no CDN) with identical sections/KPIs/per-page `<details>` (deep: 7 tables mandatory, quick: Skipped matrix row) — then ask "HTML sẵn ở `report.html` — bạn có muốn publish không? (1) `file://` sidebar `sidebar_open`, (2) `govard` static/`pub/media`, (3) `gh-pages`/`Vercel`, (4) không — tuỳ môi trường" before pushing. Templates: `references/report-template.md` + `references/report.html.template` + `references/report-theme.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).