Skip to content
Back to skills

Perf Audit

ASecurity

Performance audit: bundle size, lazy loading, data fetching, caching, N+1 queries, image optimization. Native mode checks memory, I/O, launch weight, energy.

  • 4 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 9, 2026
developmentpythonrustgojavarubyswiftkotlinshellbashsql

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 9, 2026

npx -y skills add marcoguillermaz/Tierward --skill perf-audit --agent claude-code

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.

Security grade badge for Perf Audit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/marcoguillermaz-perf-audit-tierward/badge)](https://www.skillsdirectory.com/skills/marcoguillermaz-perf-audit-tierward)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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`
> - 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:

"Run all 9 checks below. For each: state total match count, list every match as `file:line - excerpt`, and state PASS or FAIL. Adapt grep patterns to the project's framework - the concepts are universal, the specific APIs vary.

**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).
- For SSR frameworks (Next.js, Nuxt, SvelteKit): grep for client-side directives (`'use client'`, `<script>` with `client:only`, etc.) and check if the file uses any browser-specific API.
- 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 (e.g. `useEffect`, `onMounted`, `onMount`) 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 (e.g. `React.lazy`, `next/dynamic`, `defineAsyncComponent`, dynamic `import()`)."

---

## Step 3 - Bundle composition check (main context)

Read the framework configuration file (e.g. `next.config.ts`, `vite.config.ts`, `webpack.config.js`, `nuxt.config.ts`).

**P1 - Bundle analyzer availability**
Check if a bundle analyzer is configured or available for the project's build tool:
- Webpack: `webpack-bundle-analyzer` or `@next/bundle-analyzer`
- Vite: `rollup-plugin-visualizer`
- Next.js 16+: `npx next experimental-analyze` (built-in)
- Other: check for any bundle analysis tooling in devDependencies
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 (e.g. `.select('*')`, `SELECT *`, `.findAll()` without field projection, `.all()` without `only()`).
Flag: each match. Fetching all columns is a performance and security risk - columns with large values (e.g. `body`, `content`, blob URLs) 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. Each check includes grep patterns - run them, then read flagged files for context.

**Swift**:
- Main-thread work: grep for `DispatchQueue.main.sync` outside UI layer files - synchronous dispatch on main blocks the UI. Also grep for `URLSession.shared.data` without `Task { }` wrapper in non-async contexts.
- Image downsampling: grep for `UIImage(named:` or `NSImage(named:` in collection view / table view code - flag if no `preparingThumbnail` or `CGImageSource` downsampling nearby.
- Core Data batch size: grep for `NSFetchRequest` - flag if `fetchBatchSize` is 0 or not set (default fetches all objects into memory).
- Retain cycles: grep for closures capturing `self` - flag `{ self.` or `{ [self]` without `[weak self]` in long-lived contexts (completion handlers, observers, timers).
- Allocation spikes: grep for object creation inside `for`/`while` loops (e.g. `= String(`, `= Data(`, `= NSMutableAttributedString(`).

**Kotlin**:
- Main-thread DB: grep for `Room` or `SQLite` calls outside `Dispatchers.IO` or `withContext(Dispatchers.IO)`.
- RecyclerView recycling: grep for `onBindViewHolder` - flag if view inflation (`inflate(`) happens inside bind instead of `onCreateViewHolder`.
- Bitmap memory: grep for `BitmapFactory.decode` - flag if no `inSampleSize` option is set (loads full-resolution bitmaps).
- Coroutine leaks: grep for `GlobalScope.launch` - flag each occurrence (no lifecycle-aware cancellation).
- StrictMode: grep for `StrictMode` in Application class - flag if absent in debug build (disk/network violations on main thread go undetected).

**Rust**:
- Unnecessary clone: grep for `.clone()` - flag if the value is only read after cloning (borrow would suffice).
- Hot-path allocation: grep for `Vec::new()`, `String::new()`, `Box::new(` inside loop bodies - flag if the allocation could be hoisted or reused.
- Dynamic dispatch: grep for `Box<dyn` - flag if the trait has a single implementor (generics avoid vtable overhead).
- Missing inline: grep for `pub fn` in hot-path modules with body < 5 lines - consider `#[inline]` for small frequently-called functions.

**Go**:
- Goroutine creation in loops: grep for `go func` or `go ` inside `for` - flag if no semaphore/pool limits the concurrency.
- Channel sizing: grep for `make(chan` - flag unbuffered channels (`make(chan T)`) in producer-consumer patterns (causes blocking).
- Defer in loops: grep for `defer` inside `for` - deferred calls accumulate until the function returns, not the loop iteration.
- Escape analysis: recommend running `go build -gcflags='-m' 2>&1 | grep 'escapes to heap'` on hot-path packages.

**Python**:
- Regex in loops: grep for `re.compile` or `re.search`/`re.match` with literal pattern inside `for`/`while` - flag if pattern is constant (compile once outside loop).
- List vs generator: grep for list comprehensions `[... for ... in ...]` passed to `sum(`, `max(`, `min(`, `any(`, `all(` - generator expression avoids materializing the full list.
- Deep copies: grep for `copy.deepcopy` - flag in hot paths (extremely expensive).
- GIL contention: grep for `threading.Thread` doing CPU-bound work - flag (use `multiprocessing` or `concurrent.futures.ProcessPoolExecutor` instead).

**Ruby**:
- N+1 queries: grep for `.each` followed by association access (e.g. `user.company`) - flag if no `.includes(` or `.eager_load(` on the parent query.
- String allocation: grep for string interpolation `"#{` inside loops - flag if the string could be built with `StringIO` or array join.
- GC pressure: grep for `.map { |x| x.` patterns creating intermediate arrays - flag if `.lazy.map` or `.each_with_object` would avoid allocation.

**Java**:
- Autoboxing: grep for `Integer`, `Long`, `Double` in loop variable declarations - flag (use primitive types `int`, `long`, `double`).
- String concat in loops: grep for `+=` on String variables inside loops - flag (use `StringBuilder`).
- Connection pool: grep for `DriverManager.getConnection` - flag if no connection pool (HikariCP, c3p0) is configured.
- Stream overhead: grep for `.stream().` on small collections (< 10 items) - traditional loop may be faster due to stream pipeline overhead.

**dotnet**:
- LINQ allocations: grep for `.Select(`, `.Where(`, `.ToList()` chains - flag if intermediate `.ToList()` materializes unnecessarily before final consumption.
- String concatenation: grep for `+=` on string inside loops - flag (use `StringBuilder` or `string.Join`).
- LOH fragmentation: grep for `new byte[` with size > 85000 - flag (Large Object Heap allocations are expensive to collect).
- Async overhead: grep for `async` methods that contain only a single `await` with no branching - flag as candidates for removing async wrapper (avoids state machine overhead).

---

### Step 6c - Resource footprint checks (main context)

**CHECK NR1 - Launch / startup weight**
Identify the application entry point:
- Swift: `@main` struct or `AppDelegate.didFinishLaunchingWithOptions`
- Kotlin: `Application.onCreate` or `MainActivity.onCreate`
- Rust/Go/Python/Ruby/Java/dotnet: `main()` function or entry module

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**
- Swift: grep for missing `autoreleasepool` in batch processing loops. Grep for `NSCache` or `Dictionary` used as cache without size limit.
- Kotlin: grep for `static` or `companion object` holding Activity/Context references (memory leak). Check `onDestroy` for missing listener/observer cleanup.
- Rust: grep for `Vec` that grows via `push` in a loop without `with_capacity` pre-allocation.
- Go: grep for `sync.Map` or map growth without periodic cleanup - flag unbounded maps.
- All: grep for large static/global collections that persist for the process lifetime.

**CHECK NR3 - Energy and background patterns** (mobile stacks only)
- Swift: grep for `Timer.scheduledTimer` or `DispatchSource.makeTimerSource` - flag if no `tolerance` set (tight timers prevent CPU sleep). Grep for `CLLocationManager` with `startUpdatingLocation` - flag if `startMonitoringSignificantLocationChanges` would suffice.
- Kotlin: grep for `AlarmManager.setRepeating` or `Handler.postDelayed` in loops - flag excessive wake-ups. Grep for `LocationRequest` with high frequency updates.
- Flag: any background polling pattern (repeated network calls on a timer) 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.
- Swift: check for `DEBUG` conditional code that may leak into release builds (grep for `#if DEBUG` blocks containing large test fixtures or mock data).
- Kotlin: check for `debugImplementation` dependencies accidentally in `implementation` (grep `build.gradle` for heavy debug-only libraries in the wrong configuration).
- All: grep for unused imports at module/package level - flag files importing modules they don't use (increases link time and potentially binary size).

---

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

Files in this skill

  • REPORT.md8 KB
  • SKILL.md22.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.