Install
$ agentstack add skill-ddtcorex-dev-skills-hub-magento2-performance-audit ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Magento 2 Performance Audit
This skill performs a comprehensive audit of Magento 2 performance, infrastructure, and code-level patterns.
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. Async/queue findings often point back to magento2-backend-dev.
Audit Categories
1. Infrastructure Configuration
| Check | Expected | Command | |-------|----------|---------| | Application Mode | production | bin/magento deploy:mode:show | | PHP OPcache | >= 256MB | Check php.ini | | Redis (Session) | Enabled | Check app/etc/env.php | | Redis (Cache) | Enabled | Check app/etc/env.php | | Varnish | Running + terminating HTTPS/HTTP in front of the app | Check for a running Varnish container/process and its VCL config; the exact config file is project-specific (e.g. nginx.conf, a Docker/orchestrator config file, or a cloud provider's Varnish config) — there is no universal filename to grep for. |
> On a local dev environment it's normal and expected to have no Redis/Varnish at all (file-based session/cache, FPC often disabled) — don't flag this as a bug unless the target is staging/production. Confirm which environment you're actually auditing before treating any of the above as a problem.
2. Cache Configuration
# Verify all caches enabled
govard sh -c "bin/magento cache:status"
# Expected output
# Category Status Enabled
# config 1 1
# layout 1 1
# block_html 1 1
# full_page 1 1
> If full_page cache is flushing far more often than page saves/deploys would explain, see Cache Invalidation Efficiency Audit below — Magento's own entity-save invalidation is narrowly scoped by design; unexplained broad/frequent flushes are almost always custom observer or plugin code.
3. Indexer Configuration
# Check indexer mode (Update by Schedule is CRITICAL for performance on larger catalogs)
govard sh -c "bin/magento indexer:status"
# Switch a specific indexer to schedule mode (don't blanket-apply to all — see table below)
govard sh -c "bin/magento indexer:set-mode schedule "
# Reindex all
govard sh -c "bin/magento indexer:reindex"
| Indexer | Recommended Mode | |---------|------------------| | catalogproductprice | Update on Save for small catalogs / frequent price changes; Update by Schedule for large catalogs (thousands+ SKUs) where synchronous reindex-on-save would slow down admin saves and imports. Don't apply one rule blindly — check catalog size and how prices are updated (manual saves vs. bulk import) first. | | catalogurlcategory | Update by Schedule | | catalogcategoryproduct | Update by Schedule | | inventory | Update by Schedule | | targetrule | Update by Schedule |
Also check that cron is actually running and draining the changelog — schedule-mode indexers are only as fresh as the last successful cron run. Check crontab -l for a magento entry, and query cron_schedule for recent success rows (SELECT MAX(executed_at) FROM cron_schedule WHERE status='success') — an idle cron combined with schedule-mode indexers silently produces stale prices/URLs/inventory with no error anywhere.
4. Async Operations (message queue consumers)
Bulk APIs, async email sending, and async operations in Magento all run through message queue consumers — they don't do anything unless the consumers are actually running as processes (via cron or a supervisor), not just configured.
# List available consumers (does NOT start/enable them — just enumerates what's defined)
govard sh -c "bin/magento queue:consumers:list"
# Check whether consumers are actually running as processes
govard sh -c "ps aux | grep 'queue:consumers:start'"
# Start a specific consumer manually (for testing — production should run these via cron/supervisor)
govard sh -c "bin/magento queue:consumers:start --max-messages=100"
If no queue:consumers:start processes are running and there's no cron/supervisor job launching them, bulk operations and async email will queue up in queue_message tables and never actually process — check for this rather than assuming a config flag turns "async" on.
Read the consumer list before filing this as a routine perf finding. Most idle consumers are a performance/staleness issue (bulk operations, grid indexing, async email). But payment-related consumers (order invoicing/refunding/capture, e.g. a payment module's own *.order.invoicing/*.order.refunding queues) or inventory-reservation consumers being idle are a business-critical issue, not a performance one — invoices, refunds, or stock reservations silently never processing has direct financial/customer impact. Scan the consumer names for payment/inventory keywords and flag those separately at higher severity than a generic "consumers aren't running" note.
5. Asset Optimization
# JS Bundling (recommended for production)
govard sh -c "bin/magento config:set dev/js/enable_js_bundling 1"
govard sh -c "bin/magento config:set dev/js/minify_files 1"
# CSS Minification
govard sh -c "bin/magento config:set dev/css/minify_files 1"
Core Web Vitals Audit
Chrome DevTools MCP (preferred, if available)
When a Chrome DevTools MCP server is connected, performance_start_trace (with reload: true, autoStop: true) on a navigated page gives per-navigation LCP/CLS/TTFB plus an LCP phase breakdown (TTFB/load delay/load duration/render delay) and named insights (Cache, ThirdParties, RenderBlocking, ImageDelivery, etc.) in one call — no separate install, and cleaner numbers than parsing a Lighthouse report. Run it once per page type (homepage/product/category — see Per-Page-Type Audit below), since LCP/CLS meaningfully differ by page type just like everything else in this skill.
Lighthouse CI (fallback — CI pipelines, or no MCP available)
# Install Lighthouse CI
npm install -g @lhci/cli
# Run audit
lhci autorun --collect.url=https://your-store.test \
--collect.numberOfRuns=3 \
--assert.preset=desktop
Core Web Vitals Thresholds
| Metric | Target | Warning | Critical | |--------|--------|---------|---------| | LCP (Largest Contentful Paint) | 4s | | INP (Interaction to Next Paint) | 500ms | | CLS (Cumulative Layout Shift) | 0.25 | | FCP (First Contentful Paint) | 3s | | TTFB (Time to First Byte) | 1800ms |
Manual Testing
Open Chrome DevTools > Lighthouse:
- Select "Navigation" mode
- Select "Mobile" and "Desktop"
- Check all categories
- Review opportunities
Database Query Profiling
> Always verify what you actually captured, not just that curl returned something. A curl with a bare Accept: text/html and no User-Agent (curl's own default) does not behave like a real browser request on every project — on one real audit, that exact combination silently routed into a REST/webapi content-negotiation edge case and returned a fatal 500 error page instead of the real page, on every single page type, while a real browser hitting the identical URL got a normal 200. The captured body still "looked like" a page (it had HTML, a stack of queries, a profiler table) — nothing about the capture itself signaled failure. The query counts from that 500 page were reported as real findings and were wrong by 20–70×. Two non-negotiable habits prevent this: > 1. Check the HTTP status code on every captured request (-w "%{http_code}") and treat anything other than 200 as a failed capture, not data — never analyze a body you haven't confirmed the status of. > 2. Use a realistic browser Accept header and User-Agent, not framework-minimum ones, so the request exercises the same code path a real visitor hits: > ``bash > UA="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" > ACCEPT="text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" > ` > Every curl` command below assumes these two variables are set first.
Query Count: Tiers, Not a Pass/Fail Gate
A flat "under 80 on homepage, under 150 on category/product" number looks precise but isn't realistic for a real commerce Magento build. Vanilla Magento might hit those numbers, but a project with a typical real-world extension stack — payment gateways, GDPR/compliance, personalization, sorting/merchandising, feeds, the kind of thing every serious Magento business runs — routinely adds its own per-extension query overhead on top, and there's no single number that separates "healthy stack of extensions" from "one of them has a bug." Read the total as a tiered signal instead:
| Range | Read | |-------|------| | 50–150 | Vanilla/near-vanilla Magento | | 150–500 | Typical for a handful of extensions — review the top repeated shapes, don't assume it's fine just because it's under some number | | 500–1,500 | Heavy extension stack — investigate the top 5 repeated shapes specifically; may be legitimate cumulative cost or may hide a bug | | 1,500+ | Almost certainly one or more real N+1 bugs on top of extension overhead, not extension overhead alone |
Establish a project-specific baseline instead of judging against a universal number. The first time you audit a project, capture the query count as-is (after fixing anything the audit finds) and record it as that project's baseline. On every subsequent audit, compare against that baseline, not the table above — a regression from 400 to 900 matters regardless of which tier both numbers fall in, and a number that's always been 900 is a different conversation from a fresh spike to 900. The tiered table is a starting point for a project with no recorded baseline yet; the baseline is what actually matters once one exists.
The repeated-shape and cross-page-type signals elsewhere in this skill remain the primary diagnostic either way — they tell you what to fix regardless of which tier the total falls into.
DB Query Log Setup
# Enable full query logging with call stacks (see caveat below on log size)
govard sh -c "bin/magento dev:query-log:enable --include-all-queries=true --include-call-stack=true --query-time-threshold=0"
# Visit the page(s) to capture queries — output goes to var/debug/db.log (plain text, NOT *.sql)
# Format per entry: a "## ## QUERY" header (the connection id varies, so don't
# anchor a grep on a bare "## QUERY" — it will never match), then "SQL: ...", "AFF: ",
# "TIME: ", then (if --include-call-stack=true) a full PHP call stack — use the stack
# to trace a repeated/slow query back to the exact file:line that issued it.
# Count queries for one page load: clear the log, hit the page once, count entries
govard sh -c "> var/debug/db.log"
code=$(curl -sk -H "Accept: $ACCEPT" -A "$UA" -o /dev/null -w "%{http_code}" https://store.test/)
[ "$code" = "200" ] || { echo "ABORT: got HTTP $code, not 200 — this capture is not valid data"; }
govard sh -c "grep -c '## QUERY' var/debug/db.log"
# ALWAYS disable when done — this is expensive and grows fast (a single page load with
# --include-call-stack=true can produce several MB of log; on a bigger page ~10+ MB is normal)
govard sh -c "bin/magento dev:query-log:disable"
Common Query Issues
| Issue | Pattern | Impact | |-------|---------|--------| | N+1 Query | foreach with ->load() inside, or the same normalized query shape (ignore literal values) appearing dozens of times in var/debug/db.log for one page load | High | | Full Collection Load | count($collection) | Medium | | Missing Index | WHERE unindexed_column | High | | Expensive Join | Multiple JOINs on large tables | Medium |
> Check the call stack's namespace before deciding how to fix. A repeated query traced back to vendor//... (a paid extension, not vendor/magento/) isn't yours to patch directly — check for a newer version of that extension first, and if none fixes it, wrap the offending call with a request-level memoization layer (a plugin/decorator that caches the result for the current request) rather than editing vendor code, which a composer update will silently overwrite.
Query Analysis Commands
# Show slow queries (requires MySQL slow_query_log)
govard db query "SHOW FULL PROCESSLIST"
> Local dev DBs are small and fast — the absolute query time on a local box will often look fine (tens of milliseconds total) even when the query count is far over budget. Raw count is what matters here: on production, the same N+1 pattern pays a real network round-trip per query (even ~0.3–1ms same-datacenter) against much larger tables, so a high count on a fast local DB is still a real finding, not a false positive — don't dismiss it just because the local timing looks fine.
HTML Profiler (per-request timing breakdown)
# Enable the code profiler with HTML output
govard sh -c "bin/magento dev:profiler:enable html"
# IMPORTANT: the profiler only activates if the request's Accept header contains "text/html" —
# a bare `curl -s` without this header will produce NO profiler output at all (this is checked
# in app/bootstrap.php). Always include it, along with a realistic UA/Accept (see warning above)
# and a status check:
curl -sk -H "Accept: $ACCEPT" -A "$UA" -o page.html -w "%{http_code}\n" https://store.test/
# The profiler table is appended near the end of the HTML response body (a
# `...` with columns: Timer Id, Time, Avg, Cnt, Emalloc, RealMem).
# Timer Id values use "->" as a nesting separator and are also embedded in each cell's
# `title="..."` attribute — if parsing programmatically, match on `(.*?)`,
# not a naive `]*>`, since the nesting arrows inside the attribute value will break a
# naive parser that treats any ">" as the tag's end.
# Disable when done
govard sh -c "bin/magento dev:profiler:disable"
Per-Page-Type Audit (homepage, product, category)
A single-page spot check isn't representative — different page types have very different bottleneck shapes (a CMS-heavy homepage vs. a layout-heavy product page vs. a grid-heavy category page). Audit at least one of each of these three page types, using both the HTML profiler and the query log together, with full_page, block_html, and layout caches disabled so you're measuring true cache-miss cost (the worst case every real cache-miss/deploy/flush pays) rather than a warm-cache request that tells you almost nothing.
0. Pick genuinely representative pages first
Before measuring anything, verify the specific URLs you're about to test aren't degenerate cases — this is the single easiest way to get a misleading audit:
# Category: confirm it actually has products assigned (an empty category renders no grid,
# no pagination, no real layered-nav facets, and will understate real page cost)
govard db query "SELECT COUNT(*) FROM catalog_category_product WHERE category_id="
# Product: confirm it's assigned to a website (unassigned products 404 / aren't routable)
govard db query "SELECT * FROM catalog_product_website WHERE product_id="
# Product: also watch for url_rewrite entries that 301/302 redirect elsewhere (including,
# in some data sets, out to a live production domain) — follow redirects manually first,
# don't blindly -L through them into a request against someone's production site
curl -sk -o /dev/null -w "%{http_code} -> %{redirect_url}\n" https://store.test/.html
Pick a category with a normal/median product count (not the largest root category, not an edge case), and a product that resolves 200 directly.
1. Set up the uncached measurement environment
govard sh -c "bin/magento dev:profiler:enable html"
govard sh -c "bin/magento dev:query-log:enable --include-all-queries=true --include-call-stack=true --query-time-threshold=0"
govard sh
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [ddtcorex](https://github.com/ddtcorex)
- **Source:** [ddtcorex/dev-skills-hub](https://github.com/ddtcorex/dev-skills-hub)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.