SuperAPI AI Directory

skill-performance-audit

Coding & development

View repository

Updated 10-16 06:23

About this skill

Overview

This skill performs a systematic performance audit of any codebase. When invoked, it scans backend API code, database schemas, ORM queries, frontend components, and connection pool configuration for observable performance anti-patterns. It produces a severity-ranked audit report with before/after code fixes for the top bottlenecks found.

Trigger phrases: "audit performance", "find bottlenecks", "why is this slow", "performance review", "optimize this codebase", "check N+1 queries", "missing indexes".

Parameters

The agent requires access to the project's source files. Before starting the scan, confirm:

Input Required Description
Project source directory Yes The root path or src/ folder to scan
Framework / stack Auto-detected Detected from package.json, config files, file extensions
Scope Optional Limit scan to backend, frontend, or database only

If no project path is provided, ask the user to specify the directory or share the relevant files.

Returns

This skill produces a structured Performance Audit Report containing:

  • Stack detected (backend, frontend, database)
  • Severity-ranked findings: CRITICAL / WARNING / NOTICE
  • For each finding: file path, line number, pattern description, and performance impact
  • Top 3 Quick Wins with before/after code snippets

Error Handling

Situation Action
No source files found Ask the user to confirm the project path; check for non-standard source directories
Unsupported language/framework Report which checks are unsupported; run applicable checks only
File too large to read fully Sample the file and note that a partial scan was performed
No bottlenecks found Report "No issues found in scanned scope" — do not fabricate findings

Core Philosophy: Find Real Bottlenecks, Not Hypothetical Ones

This skill audits observable code patterns that are proven to cause production slowdowns. It does NOT recommend speculative micro-optimizations or premature abstractions.

What We Audit

  • Backend & API — blocking I/O, missing pagination, absent caching, N+1 queries
  • Database Connections — pool exhaustion, connection leaks, missing timeouts, long transactions
  • Database / ORM — missing indexes, over-fetching, unbounded queries
  • Big-O Complexity — quadratic loops, linear searches replaceable with O(1) Maps/Sets, exponential recursion, repeated sorting
  • Frontend — render thrashing, bundle bloat, missing lazy loading, unoptimized assets
  • Over-Engineering — empty pass-through layers, premature generalization, unnecessary infrastructure, unjustified design patterns

What We Do NOT Do

  • Recommend rewrites of working code without evidence
  • Flag code that "might" be slow without a clear anti-pattern
  • Suggest caching where traffic data is unknown
  • Report style issues as performance issues

Step 1 — Context Discovery

Before scanning, identify the project's stack to apply the correct checks.

Detect Backend

Look for these signals:

Signal Framework/Runtime
express, fastify in package.json Node.js / Express / Fastify
@nestjs/core in package.json NestJS
requirements.txt with django or flask Python
pom.xml or build.gradle Java / Spring
Gemfile with rails Ruby on Rails

Detect Frontend

Look for these signals:

Signal Framework
react, react-dom in package.json React
next in package.json Next.js
vue in package.json Vue.js
@angular/core in package.json Angular

Detect Database / ORM

Look for these signals:

Signal ORM / DB
schema.prisma file present Prisma
@Entity or typeorm in code TypeORM
mongoose in package.json Mongoose / MongoDB
.sql files or raw pg, mysql2 in package.json Raw SQL

Capture the full stack before proceeding. Skip checks that are irrelevant to the detected stack.


Step 2 — The Bottleneck Scan

Run these checks in order. For each finding, note the file path and line numbers.

BACKEND CHECKS

B1 — N+1 Queries

Pattern: A database call inside a loop (for, while, forEach, map, Promise.all over individual queries).

What to look for:

// PROBLEM — fires one query per item
for (const userId of userIds) {
  const user = await db.user.findUnique({ where: { id: userId } });
}

// ALSO PROBLEM — Promise.all with individual queries is still N+1
await Promise.all(userIds.map(id => db.user.findUnique({ where: { id } })));

Severity: CRITICAL — on large datasets, this converts O(1) endpoints into O(n) database hammers.

Fix reference: references/backend-bottlenecks.md#n-plus-one


B2 — Synchronous Blocking in Request Handlers

Pattern: Synchronous file system, crypto, or compression calls inside HTTP route handlers.

What to look for:

// PROBLEM — blocks the entire Node.js event loop
app.get('/data', (req, res) => {
  const content = fs.readFileSync('./large-file.json');       // BLOCKING
  const hash = crypto.createHash('md5').update(data).digest('hex');  // OK for small data
  const compressed = zlib.gzipSync(largeBuffer);              // BLOCKING
});

Severity: CRITICAL — a single synchronous call blocks ALL concurrent requests.

Fix reference: references/backend-bottlenecks.md#sync-blocking


B3 — Unpaginated Endpoints

Pattern: Queries that return all records without a limit, take, or pagination parameter.

What to look for:

// PROBLEM — returns every row, always
const users = await db.user.findMany();
const orders = await Order.find({});
const results = await db.query('SELECT * FROM products');

Severity: WARNING — harmless in dev, catastrophic in production with 100k+ rows.

Fix reference: references/backend-bottlenecks.md#pagination


B4 — Missing Caching on Expensive Operations

Pattern: Identical external API calls or heavy computations re-executed on every request with no cache layer.

What to look for:

  • External API calls (axios.get, fetch) inside route handlers with no cache check
  • CPU-heavy transforms (sorting, aggregating large arrays) inside hot paths
  • No Redis, in-memory Map, or HTTP cache headers on read-heavy endpoints

Severity: WARNING — depends on traffic; becomes CRITICAL under load.

Fix reference: references/backend-bottlenecks.md#caching


B5 — Unhandled Promise Rejections / Missing Error Boundaries

Pattern: async functions without try/catch that can crash the process silently.

What to look for:

// PROBLEM — unhandled rejection crashes Node.js process
app.get('/user/:id', async (req, res) => {
  const user = await db.user.findUnique({ where: { id: req.params.id } }); // no try/catch
  res.json(user);
});

Severity: WARNING — causes silent 500s and potential memory leaks from dangling promises.


CONNECTION CHECKS

C1 — Connection Pool Exhaustion

Pattern: More concurrent requests than available database connections. New requests hang waiting for a connection to free up, causing cascading timeouts.

What to look for:

  • Pool size not configured (uses ORM defaults, often too small for production)
  • Long-running queries or transactions holding connections open unnecessarily
  • No connection timeout configured (requests queue forever)
// PROBLEM — default Prisma pool is usually 5 connections; may be too small
const db = new PrismaClient();

// PROBLEM — TypeORM default pool of 10 is often not reviewed
createConnection({ type: 'postgres', ... }); // no pool config

// PROBLEM — creating a new pool/client per request instead of sharing one
app.get('/users', async (req, res) => {
  const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // new pool every request!
  const result = await pool.query('SELECT * FROM users');
});

Severity: CRITICAL — under load, a pool of 5 with 100 concurrent requests means 95 requests stall and time out.

Fix reference: references/connection-bottlenecks.md#pool-exhaustion


C2 — Connection Leaks (Not Releasing Connections)

Pattern: Connections acquired from the pool that are never released back — because of an unhandled error, early return, or missing finally block.

What to look for:

// PROBLEM — if query throws, client is never released back to pool
const client = await pool.connect();
const result = await client.query('SELECT ...'); // throws → client leaked
client.release();

// PROBLEM — TypeORM QueryRunner not released in error path
const qr = dataSource.createQueryRunner();
await qr.connect();
await qr.startTransaction();
await qr.manager.save(entity); // if throws, release() is never called
await qr.commitTransaction();
await qr.release();

Severity: CRITICAL — each leaked connection permanently reduces pool capacity. Under load, pool empties completely and all requests hang.

Fix reference: references/connection-bottlenecks.md#connection-leaks


C3 — Missing Connection Timeout Configuration

Pattern: No connectionTimeoutMillis, acquireTimeoutMillis, or equivalent configured — so requests wait indefinitely for a pool slot instead of failing fast.

What to look for:

// PROBLEM — no timeout; requests queue forever if pool is full
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// PROBLEM — Prisma without connection limit or timeout hints
const db = new PrismaClient();
// Should use: DATABASE_URL="...?connection_limit=10&pool_timeout=10"

Severity: WARNING — without timeouts, a DB slowdown causes all API requests to hang until the server runs out of memory.

Fix reference: references/connection-bottlenecks.md#timeouts


C4 — Long Transactions Blocking Other Queries

Pattern: A transaction that holds locks for too long — because it includes slow external calls, large batch operations, or is never committed.

What to look for:

// PROBLEM — HTTP call inside a transaction holds DB lock for network RTT
await db.$transaction(async (tx) => {
  const user = await tx.user.findUnique({ where: { id } });
  const response = await fetch('https://external-api.com/validate'); // holds lock during HTTP call
  await tx.user.update({ where: { id }, data: { verified: true } });
});

// PROBLEM — large batch loop inside a single transaction
await db.$transaction(async (tx) => {
  for (const item of tenThousandItems) {
    await tx.item.create({ data: item }); // locks table for entire duration
  }
});

Severity: WARNING — long locks block concurrent writes/reads on affected rows, causing request pile-ups.

Fix reference: references/connection-bottlenecks.md#long-transactions


DATABASE CHECKS

D1 — Missing Indexes on Frequently Queried Fields

Pattern: Fields used in WHERE, JOIN, or ORDER BY clauses that lack @index, @unique, or explicit index declarations.

What to look for in Prisma schema:

model User {
  id        String   @id
  email     String   // PROBLEM — no @unique or @@index; queried constantly
  createdAt DateTime // PROBLEM — if used in ORDER BY, needs an index
}

What to look for in TypeORM:

@Column()
email: string; // PROBLEM — missing @Index() decorator

Severity: CRITICAL on tables > 10k rows. Full table scans grow with O(n) complexity.

Fix reference: references/database-bottlenecks.md#missing-indexes


D2 — Over-Fetching (SELECT *)

Pattern: Queries that pull all columns when only a subset is needed.

What to look for:

// PROBLEM — pulls blob columns, unused fields, everything
const user = await db.user.findUnique({ where: { id } });
// vs. select only what's needed
const user = await db.user.findUnique({ where: { id }, select: { id: true, email: true } });

Severity: WARNING — especially costly when tables have TEXT, JSON, or BLOB columns.

Fix reference: references/database-bottlenecks.md#over-fetching


D3 — Missing Transactions for Multi-Step Writes

Pattern: Multiple related write operations performed without a database transaction, leaving the system in an inconsistent state on partial failure.

What to look for:

// PROBLEM — if second write fails, first write is orphaned
await db.order.create({ data: orderData });
await db.inventory.update({ ... }); // if this throws, order exists with no inventory deduction

Severity: WARNING — data integrity risk that becomes CRITICAL in financial or inventory systems.


BIG-O COMPLEXITY CHECKS

These checks identify algorithmic complexity issues — patterns where the time or space cost of an operation scales poorly as input size grows. Each finding includes the detected complexity class and the practical breaking point.

O1 — Nested Loops Over the Same Dataset (O(n²))

Pattern: A loop inside a loop over collections of the same or related data. Performance degrades quadratically: doubling the input quadruples the time.

What to look for:

// PROBLEM — O(n²): for each user, scan all permissions
for (const user of users) {
  for (const permission of permissions) {
    if (permission.userId === user.id) { // should use a Map lookup instead
      user.perms.push(permission);
    }
  }
}

// PROBLEM — O(n²): Array.includes() inside a loop (includes is O(n) itself)
const filtered = items.filter(item => excludedIds.includes(item.id));

// PROBLEM — O(n²): indexOf inside a loop
for (const item of largeArray) {
  if (anotherLargeArray.indexOf(item.id) !== -1) { ... }
}

Severity: CRITICAL when n > 1,000. At n=10,000: O(n) = 10k ops, O(n²) = 100M ops.

Fix reference: references/complexity-bigO.md#quadratic


O2 — Linear Search on Large Collections That Could Be O(1) (O(n) → O(1))

Pattern: Using Array.find(), Array.filter(), or Array.includes() repeatedly on the same large array, when the array should be converted to a Map or Set once for O(1) lookups.

What to look for:

// PROBLEM — O(n) per lookup, called in a loop = O(n²) total
function getUser(id: string) {
  return users.find(u => u.id === id); // scans array every call
}

// PROBLEM — repeated filter on same array
const admins = users.filter(u => u.role === 'admin');
const mods = users.filter(u => u.role === 'mod'); // second full scan

Severity: WARNING — becomes CRITICAL when called inside loops or hot paths.

Fix reference: references/complexity-bigO.md#linear-to-constant


O3 — Uncontrolled Recursion Without Memoization (Exponential Risk)

Pattern: Recursive functions that recompute the same sub-problems without caching results — or that lack a proper base case / depth limit.

What to look for:

// PROBLEM — O(2ⁿ) without memoization: recomputes fib(n-1) and fib(n-2) from scratch
function fib(n: number): number {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2); // exponential time
}

// PROBLEM — recursive tree traversal with no depth guard
function flatten(node: TreeNode): Item[] {
  return [node.value, ...node.children.flatMap(flatten)]; // no depth limit
}

Severity: CRITICAL if called with unbounded input. fib(50) makes ~2²⁵ calls (~33M).

Fix reference: references/complexity-bigO.md#recursion


O4 — Repeated Sorting of the Same Data (O(n log n) wasted)

Pattern: Sorting the same array on every function call, request, or render cycle instead of sorting once and caching or maintaining a sorted structure.

What to look for:

// PROBLEM — sorts entire product list on every search keystroke
function search(query: string) {
  const sorted = products.sort((a, b) => b.price - a.price); // re-sort every call
  return sorted.filter(p => p.name.includes(query));
}

// PROBLEM — sorting inside a route handler with no cache
app.get('/leaderboard', async (req, res) => {
  const scores = await db.score.findMany();
  scores.sort((a, b) => b.points - a.points); // sorted fresh every request
  res.json(scores);
});

Severity: WARNING — at 10k records, sort is ~130k comparisons. In a hot path this adds up fast.

Fix reference: references/complexity-bigO.md#repeated-sorting


FRONTEND CHECKS

F1 — Expensive Computations Without Memoization

Pattern: Heavy calculations or array transforms inside React/Vue component render functions that re-run on every render.

What to look for:

// PROBLEM — re-sorts 10k items on every keystroke, mouse move, etc.
function ProductList({ products }) {
  const sorted = products.sort((a, b) => b.price - a.price); // re-runs every render
  return <ul>{sorted.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

Severity: WARNING — causes dropped frames and janky UI on low-end devices.

Fix reference: references/frontend-bottlenecks.md#memoization


F2 — Missing Lazy Loading for Heavy Components

Pattern: Large components (charts, maps, rich text editors, modals) imported synchronously at the top of the file, bloating the initial bundle.

What to look for:

// PROBLEM — Chart.js is ~200KB and is loaded even for users who never see the chart
import { Chart } from 'chart.js';
import ReactQuill from 'react-quill'; // rich text editor, large bundle

Severity: WARNING — directly increases Time to Interactive (TTI) for all users.

Fix reference: references/frontend-bottlenecks.md#lazy-loading


F3 — Unoptimized Images

Pattern: Raw <img> tags with large static images, no width/height hints, no lazy loading, or no modern format (WebP/AVIF).

What to look for:

// PROBLEM — in a Next.js project, bypasses all optimization
<img src="/hero.jpg" />

// ALSO PROBLEM — no lazy loading on below-the-fold images
<img src="/product.png" loading="eager" />

Severity: WARNING — images are typically 60-80% of page weight. Critical for LCP score.

Fix reference: references/frontend-bottlenecks.md#images


F4 — Bundle Bloat from Whole-Library Imports

Pattern: Importing an entire library when only a single function or component is needed.

What to look for:

// PROBLEM — imports all 200+ lodash functions (~70KB gzipped)
import _ from 'lodash';
const result = _.groupBy(items, 'category');

// PROBLEM — imports all of date-fns
import * as dateFns from 'date-fns';

Severity: WARNING — inflates JavaScript bundle size, hurting TTI for all users.

Fix reference: references/frontend-bottlenecks.md#bundle-bloat


F5 — Prop Drilling / Context Overuse Causing Full-Tree Re-renders

Pattern: React Context used to share frequently-updating state (e.g., scroll position, form input) causes entire component subtrees to re-render.

What to look for:

  • A single Context.Provider wrapping the entire app with state that changes frequently
  • Components deeply nested under a provider that re-render on unrelated state changes

Severity: NOTICE — but can become WARNING in large trees with many expensive child components.


OVER-ENGINEERING CHECKS

Over-engineering introduces accidental complexity — abstractions, patterns, and infrastructure that cost more to maintain than the value they deliver. This section detects patterns where the architectural overhead outweighs the actual problem being solved.

Rule: Flag over-engineering only when evidence of unnecessary complexity is observable, not when code "could be simpler."

X1 — Abstraction Layers With No Behavior (Empty Pass-Through)

Pattern: Classes, interfaces, or services that add a layer of indirection without adding logic, validation, or transformation — pure delegation chains.

What to look for:

// PROBLEM — service does nothing but call the repository
class UserService {
  constructor(private readonly userRepo: UserRepository) {}

  async getUser(id: string) {
    return this.userRepo.findById(id); // zero added value
  }

  async createUser(data: CreateUserDto) {
    return this.userRepo.save(data); // no validation, no domain logic, no mapping
  }
}

// PROBLEM — repository re-wraps the ORM with no value
class UserRepository {
  async findById(id: string) {
    return this.prisma.user.findUnique({ where: { id } }); // one-liner that could be inline
  }
}

Severity: NOTICE — but becomes WARNING when there are 3+ pass-through layers (controller → service → repository → ORM) with zero logic at any intermediate layer.

Fix reference: references/over-engineering.md#empty-abstractions


X2 — Premature Generalization (Over-Parametrized Functions)

Pattern: Functions with many optional parameters, strategy objects, or plugin hooks designed for flexibility that is never actually used.

What to look for:

// PROBLEM — 7 options for a function called with defaults 100% of the time
interface FetchOptions {
  retries?: number;
  timeout?: number;
  cache?: boolean;
  cacheKey?: string;
  cacheTtl?: number;
  transform?: (data: unknown) => unknown;
  validate?: (data: unknown) => boolean;
}

async function fetchData(url: string, options: FetchOptions = {}) { ... }

// Called everywhere as:
const data = await fetchData('/api/users'); // never uses any options

Severity: NOTICE — flag when: the function has 5+ optional parameters AND the non-default usage is 0 or 1 call site.

Fix reference: references/over-engineering.md#premature-generalization


X3 — Infrastructure Introduced Before the Problem Exists

Pattern: Heavy infrastructure (message queues, event buses, microservices split, CQRS) introduced without evidence of the scaling problem it solves.

What to look for:

  • A RabbitMQ/Kafka/SQS queue that publishes to a single consumer that could be a direct function call
  • CQRS (separate read/write models) on a single-database app with no read/write ratio problem
  • An event bus (EventEmitter subclass with 20 event types) for a single-server app
  • Microservices with shared databases (negates the main benefit of microservices)
// PROBLEM — event bus for what is literally a sequential flow
eventBus.emit('user.created', user);
// ... in another file ...
eventBus.on('user.created', async (user) => {
  await sendWelcomeEmail(user); // could just be called directly after createUser()
});

Severity: WARNING — over-infrastructure adds operational cost (deployment complexity, debugging, latency) with no benefit at current scale.

Fix reference: references/over-engineering.md#premature-infrastructure


X4 — Design Patterns Applied Without Justification

Pattern: Classic Gang-of-Four or enterprise patterns (Factory, Strategy, Observer, Decorator, Unit of Work) applied to scenarios too simple to need them.

What to look for:

// PROBLEM — Factory for a class with a single implementation
interface IUserFactory {
  create(data: CreateUserDto): User;
}
class UserFactory implements IUserFactory {
  create(data: CreateUserDto): User {
    return new User(data.id, data.email, data.name); // just use `new User()` directly
  }
}

// PROBLEM — Strategy pattern for a switch with 2 cases that won't grow
interface SortStrategy { sort(items: Item[]): Item[]; }
class AscendingSort implements SortStrategy { ... }
class DescendingSort implements SortStrategy { ... }
// Used as: new Sorter(new AscendingSort()).sort(items)
// Should be: items.sort((a, b) => a.name.localeCompare(b.name))

// PROBLEM — Unit of Work wrapping a single ORM that already handles transactions
class UnitOfWork {
  constructor(private userRepo: UserRepo, private orderRepo: OrderRepo) {}
  async commit() { /* wraps Prisma.$transaction */ }
}

Severity: NOTICE — flag when the pattern adds more lines than the problem it solves. Patterns are justified when there are 3+ implementations or clear extension points in use.

Fix reference: references/over-engineering.md#unnecessary-patterns


Step 3 — Audit Report

After running all applicable checks, produce this exact output format:

╔══════════════════════════════════════╗
║      PERFORMANCE AUDIT REPORT        ║
╚══════════════════════════════════════╝

STACK DETECTED:
  Backend:  <detected or "not found">
  Frontend: <detected or "not found">
  Database: <detected or "not found">

────────────────────────────────────────
🔴 CRITICAL  (<count> found)
────────────────────────────────────────
[B1] N+1 Query
  File: src/services/user.service.ts:42
  Detail: Loop over userIds fires one findUnique per iteration.
  Impact: 100 users = 101 DB queries. 10k users = 10,001 DB queries.

[D1] Missing Index
  File: prisma/schema.prisma:12 (User.email)
  Detail: email is used in WHERE clauses but has no @unique or @@index.
  Impact: Full table scan on every login attempt.

────────────────────────────────────────
🟡 WARNING   (<count> found)
────────────────────────────────────────
[B3] Unpaginated Endpoint
  File: src/controllers/product.controller.ts:28
  Detail: findMany() called with no take or skip.
  Impact: Returns all rows. Will OOM on large tables.

[F2] Missing Lazy Loading
  File: src/pages/dashboard.tsx:3
  Detail: ReactQuill imported synchronously.
  Impact: ~200KB added to initial bundle for all users.

────────────────────────────────────────
🔵 NOTICE    (<count> found)
────────────────────────────────────────
[F5] Context Re-render Risk
  File: src/context/AppContext.tsx
  Detail: Scroll position stored in root Context.
  Impact: Every scroll event re-renders the full app tree.

════════════════════════════════════════
⭐ TOP 3 QUICK WINS
════════════════════════════════════════
[see Quick Wins section below]

Quick Wins Output Format

After the summary table, produce a "Quick Wins" section with a before/after fix for the top 3 findings ranked by severity:

QUICK WIN #1 — [Check ID] [Check Name]
File: <path:line>

BEFORE:
  <problematic code snippet>

AFTER:
  <fixed code snippet>

WHY: <one sentence explaining the performance impact>

References

Detailed knowledge base for each bottleneck category: