Installs into .claude/skills of the current project.
Are you the author of Perf Audit?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/marcoguillermaz-perf-audit)
---
name: perf-audit
description: Performance audit: bundle size, lazy loading, data fetching, caching, N+1 queries, image optimization. Native mode checks memory, I/O, launch weight, energy.
user-invocable: true
model: sonnet
context: fork
allowed-tools: Read Glob Grep Bash Agent Edit Write
argument-hint: [target:section:<section>|target:page:<route>|mode:audit|mode:apply]
---
## Configuration (adapt before first run)
> Replace these placeholders:
> - `[FRAMEWORK]` - e.g. `Next.js App Router`, `Remix`, `SvelteKit`, `plain React`
> - `[SITEMAP_OR_ROUTE_LIST]` - e.g. `docs/sitemap.md`
> - `[API_ROUTES_PATH]` - e.g. `routes/`, `src/handlers/`, `api/`
> - `[BUNDLE_TOOL]` - e.g. `@next/bundle-analyzer`, `vite-bundle-visualizer`, `webpack-bundle-analyzer`
> - `[PERF_TOOL]` - profiling tool for native/backend stacks - e.g. `Instruments` (Swift), `Android Profiler` (Kotlin), `perf` / `flamegraph` (Rust/Go), `cProfile` (Python), `dotnet-trace` (.NET). If not configured, the skill runs static analysis only and recommends profiling setup.
> - `[PROFILER_COMMAND]` - command to generate a profile - e.g. `xcrun xctrace record --template 'Time Profiler'`, `go tool pprof`, `cargo flamegraph`, `python -m cProfile -o profile.out`. If not configured, the skill skips the profiling step and proceeds with static analysis.
> - Note: if this is a **public-facing** app, remove the "Out of scope" restriction on Core Web Vitals
## Applicability check
Before any other step: read `CLAUDE.md` and check the Framework and Language fields.
- If the project is a **web application** (Framework is NOT `N/A - native app` and NOT `N/A - no web frontend`): proceed to **Step 1 - Web Performance Audit** (Steps 1–5).
- If the project is a **native or backend-only application** (Framework is `N/A - native app` or `N/A - no web frontend`): skip Steps 1–5 and proceed directly to **Step 6 - Native/Backend Performance Audit**.
## Context and scope
**In scope**: bundle composition, server/client component boundaries, dynamic rendering triggers, lazy loading, data fetching efficiency (caching, parallelism, N+1), re-render patterns, image optimization. For native stacks: algorithmic complexity, stack-specific hot-path patterns, memory management, launch weight, energy impact, binary size.
**Out of scope**: SEO, CWV as ranking signals, `robots.txt`, `sitemap.xml`, OG tags, Lighthouse site audit, accessibility (→ `/ui-audit`), DB schema (→ `/skill-db`).
**Default mode**: audit (report only, no code changes). `mode:apply` makes focused non-breaking fixes.
**CWV reference (informational only - not primary deliverable for this internal app):**
| Metric | Good | Needs work | Poor | Primary cause |
|---|---|---|---|---|
| LCP | ≤ 2.5s | 2.5–4s | > 4s | Slow server response, render-blocking resources |
| CLS | ≤ 0.1 | 0.1–0.25 | > 0.25 | Unsized images, dynamic content above fold |
| INP | ≤ 200ms | 200–500ms | > 500ms | Blocking JS on event handlers |
Source: web.dev/articles/vitals. Use these thresholds only as triage anchors - not as primary deliverables.
---
## Step 0 - Target resolution
Parse `$ARGUMENTS` for `target:` and `mode:` tokens.
**Operating modes:**
| Mode | Behavior |
|---|---|
| `mode:audit` (default) | Report only - no code changes |
| `mode:apply` | Apply focused, non-breaking fixes (lazy loading wrappers, `Promise.all` parallelization). Describe each change before applying. |
**Target resolution:**
| Pattern | Meaning |
|---|---|
| `target:page:/dashboard` | Focus on the dashboard and its component tree (example) |
| No argument | **Full audit - ALL page files + layout files + lib files + API routes from sitemap.md. Maximum depth.** |
**STRICT PARSING - mandatory**: derive target ONLY from the explicit text in `$ARGUMENTS`. Do NOT infer target from conversation context, recent work, active block names, or project memory. If `$ARGUMENTS` contains no `target:` token → full audit across ALL files from sitemap.md at maximum depth. When a target IS provided → act with maximum depth and completeness on that specific scope only.
**Target scoping - comparative vs independent checks (mandatory):** a subset of checks evaluate a *project-wide property* (build configuration, client bundle composition, launch entry point, release artifact); the rest evaluate one file or route in isolation.
- **Comparative checks - full-project by design; the `target:` filter does NOT apply to them:** P1, P2, P3 (web) and NR1, NR4 (native). They MUST run against the full project even when a target is given. The client bundle, build configuration, entry point, and release artifact are global - narrowing them to the target subset hides cross-cutting exposure (a heavy package bundled by a non-targeted page still ships to every user).
- **Independent checks - target-safe:** every other check (B1-B9, Q1-Q3, NP1-NP4, Step 6b, NR2, NR3) evaluates a single file/route and safely honors the `target:` filter. Layout/shell files stay in scope regardless of target (Step 1).
- When a `target:` is given: apply the filter to the independent checks only; run the comparative checks across the full project and state in the report that they ran full-project by design.
Announce: `Running perf-audit - scope: [FULL | target: <resolved>] - mode: [audit | apply]`
Apply the target filter to the file list in Step 1.
---
## Step 1 - Build file inventory
- All page/view files in scope
- All heavy components (those with chart, calendar, data table, rich text editor - high rendering cost)
- All layout/shell files (always include regardless of target - critical for B1 Flag B and B8)
- All service/data-access files directly called from page/component files - exclude pure utility files with no DB calls
- Framework configuration file (e.g. `next.config.ts`, `vite.config.ts`, `nuxt.config.ts`, `webpack.config.js`)
Read `docs/refactoring-backlog.md` - note existing `PERF-` entries to avoid duplicates.
---
## Step 2 - Server/Client boundary checks (Explore agent)
Launch a **single Explore subagent** (model: haiku) with all page, component, and lib files from Step 1:
"Read `${CLAUDE_SKILL_DIR}/PATTERNS.md` for framework-specific grep patterns. Run all 9 checks below. For each: state total match count, list every match as `file:line - excerpt`, and state PASS or FAIL.
**CHECK B1 - Unnecessary client-side rendering scope**
Find components or pages marked for client-side rendering that do not actually use browser APIs (event handlers, state, refs, browser-only hooks). See PATTERNS.md → B1 for client-side directives per framework.
- Flag A: any client-marked file that uses NONE of: state management, event handlers, refs, or browser APIs. It may be renderable on the server.
- Flag B: any client-marked layout or provider file that imports 5+ child components - if only a small interactive portion requires client rendering, extracting it into an island would let the layout render on the server.
**CHECK B2 - Heavy libraries in client bundle**
Grep for imports of known-heavy libraries in client-rendered files. Flag any imports of libraries that do NOT need browser APIs and could run server-side:
- Document processing libraries (PDF, XLSX, etc.) - typically server-only
- Rich text editors - acceptable in client IF interactive; flag if read-only render
- Chart libraries - acceptable in client if interactive; flag if static data-display only
- Any library > ~50KB that has a server-side equivalent
Flag: each import with the library name and whether a server-side pattern exists.
**CHECK B3 - Client-side data fetching that defeats server rendering**
Find client-rendered components that fetch data in lifecycle hooks (see PATTERNS.md → B3) instead of using server-side data loading.
Flag: data fetching in client lifecycle hooks - these defeat server rendering and add a loading waterfall. Use server-side data loading with streaming/suspense, or a client-side cache library with proper revalidation.
**CHECK B4 - Unstable callback references defeating memoization**
Find inline arrow functions passed as props to memoized child components. Inline functions create new references on every parent render, defeating memoization.
Flag: each case. Skip native HTML element event handlers - frameworks batch these internally.
**CHECK B5 - Sequential await waterfall**
Pattern: two or more consecutive `await` calls for independent data sources in the same async function.
Grep in server-rendered files or API routes: lines matching `const .* = await` that appear consecutively (within 5 lines of each other) without the first result being an input to the second call.
Flag: each pair of sequential awaits that could be parallelised with `Promise.all` (or equivalent).
Example of violation: `const a = await getA(); const b = await getB();` where b doesn't depend on a.
**CHECK B6 - Missing caching on repeated server-side queries**
Find data fetches that run on every request without caching in server-rendered components.
Grep in server-rendered files for database query calls - check if they use any caching mechanism (framework cache, memoization, or explicit cache headers).
Flag: uncached queries in layout-level or frequently-visited components (e.g. navigation data, user profile). These execute on every page visit.
**CHECK B7 - Images without explicit dimensions (CLS risk)**
Find image elements (framework image component or native `<img>`) without width/height or fill/cover constraints.
Flag: each unsized image. Without dimensions, the browser cannot reserve layout space - content shifts when the image loads, causing CLS violations.
**CHECK B8 - Accidental dynamic rendering triggers**
Find patterns that force dynamic rendering at the layout level, opting entire route subtrees out of static generation:
- Request-scoped APIs (cookies, headers, session) called in layout files
- Explicit force-dynamic configuration in page/layout files
- No-cache directives in layout-level data fetches
Flag: each occurrence. Verify from context whether intentional (auth-dependent, real-time data) or avoidable with proper caching or component restructuring.
**CHECK B9 - Missing lazy loading for heavy client components**
Find imports of heavy libraries (rich text editors, chart libraries, complex UI components) that are statically imported without lazy loading.
Flag: any file where a heavy component is eagerly imported without using the framework's dynamic/lazy import mechanism (see PATTERNS.md → B9)."
---
## Step 3 - Bundle composition check (main context)
Read the framework configuration file.
**P1 - Bundle analyzer availability**
Check if a bundle analyzer is configured or available for the project's build tool (see PATTERNS.md → P1 for tool names per build system).
If no analyzer is configured or documented, flag as Medium - developers cannot easily audit bundle composition.
**P2 - Server-only package exclusion**
Check if the framework configuration excludes heavy server-only packages from the client bundle.
Identify packages in the project that should never be client-bundled (e.g. PDF processors, XLSX generators, database drivers, file system utilities).
Flag: any heavy server-only package not explicitly excluded from client bundling.
**P3 - Tree-shaking optimization for large libraries**
Check if the build configuration optimizes imports for large libraries with many exports where only a subset is used (e.g. icon libraries with hundreds of icons, utility libraries like lodash).
Note: some frameworks automatically optimize certain packages (check the framework docs). If a library is already auto-optimized by the build tool, note as PASS.
Flag: any package with 100+ exports where only a subset is used, not covered by the build tool's tree-shaking or import optimization.
---
## Step 4 - API query efficiency (Explore agent - separate pass)
"Run these 3 checks. Adapt grep patterns to the project's database client or ORM:
**CHECK Q1 - Unbounded queries (no limit on large-growth tables)**
Flag: each collection query without pagination bounds. Exception: export routes that intentionally fetch all for CSV/XLSX - verify from context.
**CHECK Q2 - Select * (over-fetching columns)**
Grep for select-all patterns in route handlers (see PATTERNS.md → Q2 for ORM-specific patterns).
Flag: each match. Fetching all columns is a performance and security risk - columns with large values are sent over the wire unnecessarily.
**CHECK Q3 - N+1 patterns**
Pattern A: database query calls inside `.map(`, `for`, or `forEach` loops.
Pattern B: sequential `await` with a DB client call inside a loop body.
Flag: each match. N+1 on a list endpoint means N DB queries for an N-item list - use batch queries or embedded/eager loading instead."
---
## Step 5 - Produce report and update backlog
Generate the report using the template in `${CLAUDE_SKILL_DIR}/REPORT.md` (Web Audit section). Apply the severity guide and backlog writing rules from the same file.
---
## Execution notes
- In `mode:audit` (default): do NOT make any code changes - report only. After producing the report, ask: "Should I implement the High/Critical priority optimisations?"
- In `mode:apply`: apply only the fixes listed in Quick wins (isolated, non-breaking). Describe each change before writing it. Do not apply Strategic refactors without explicit user confirmation. Do NOT ask the closing question - the user already expressed intent via `mode:apply`.
- Check CLAUDE.md "Known Patterns" for intentionally configured server-external packages, client-side component exclusions, or layout-level client directives before flagging them.
---
## Step 6 - Native/Backend Performance Audit
**This path runs for non-web projects only.** Web projects use Steps 1–5 above.
### Profiling tools
**Primary tool**: [PERF_TOOL]
**Profile command**: `[PROFILER_COMMAND]`
Run the profiler on the main execution path. Identify the top 5 hotspots by CPU time and the top 5 by memory allocation. If profiling data is not available, proceed with static analysis and recommend running the profiler.
---
### Step 6a - Algorithmic and resource checks (Explore agent)
Launch a **single Explore subagent** (model: haiku) with all source files from the project:
"Run these 4 checks on the provided source files. For each: state total match count, list every match as `file:line - excerpt`, and state PASS or FAIL.
**CHECK NP1 - Nested iteration on collections**
Grep for nested loops (for/while/map/forEach inside another loop body) operating on collections or arrays.
Flag: O(n²) or worse complexity. Exclude small fixed-size iterations (< 10 items known at compile time).
**CHECK NP2 - Memory allocation in hot paths**
Flag:
- Object/string/array/buffer creation inside loops (new allocation per iteration)
- Unbounded caches or collections that grow without eviction
- Missing cleanup of heavyweight resources (file handles, DB connections, network sockets)
**CHECK NP3 - I/O bottleneck patterns**
Flag:
- Synchronous file I/O on the main/UI thread
- Sequential network calls that could be parallelized
- Unbuffered reads/writes on large files
- Missing timeout on network or IPC operations
**CHECK NP4 - Concurrency inefficiency**
Flag:
- Thread/goroutine/task creation inside loops without pooling
- Shared mutable state without synchronization primitives
- Blocking calls on async/concurrent paths
- Missing cancellation support on long-running operations"
---
### Step 6b - Stack-specific checks (main context)
Read the 10 largest source files by line count. Apply the checks relevant to the project language using grep patterns from PATTERNS.md → Step 6b (organized by language). Run the patterns, then read flagged files for context.
---
### Step 6c - Resource footprint checks (main context)
**CHECK NR1 - Launch / startup weight**
Identify the application entry point (see PATTERNS.md → NR1 for per-language markers). Grep for heavy operations in the entry point: database initialization, network calls, large file reads, complex object graph construction. Flag any operation that could be deferred (lazy initialization) or moved to a background thread/task.
**CHECK NR2 - Memory management patterns**
Grep for per-language memory antipatterns from PATTERNS.md → NR2. Key categories: missing autorelease pools in batch loops, unbounded caches, leaked references, pre-allocation misses. Also flag large static/global collections that persist for the process lifetime.
**CHECK NR3 - Energy and background patterns** (mobile stacks only)
Grep for per-language energy antipatterns from PATTERNS.md → NR3. Key categories: tight timers without tolerance, high-frequency location updates, excessive wake-ups. Flag any background polling pattern that could use push notifications or event-driven updates instead.
**CHECK NR4 - Binary / artifact size**
- Grep for large embedded assets in source directories (images > 1MB, bundled databases, embedded fonts). Flag if assets could be downloaded on demand.
- Check for debug-only code or dependencies leaking into release builds.
- Grep for unused imports at module/package level - flag files importing modules they don't use.
---
### Step 6d - Produce report and update backlog
Generate the report using the template in `${CLAUDE_SKILL_DIR}/REPORT.md` (Native Audit section). Apply the severity guide and backlog writing rules from the same file.
---
### Native audit - execution notes
- In `mode:audit` (default): report only. After report, ask: "Should I implement the High/Critical priority optimisations?"
- In `mode:apply`: apply only Quick wins. Describe each change before writing.