Comprehensive guide to all performance issue detectors in Code Evolution Lab.
Overview
Code Evolution Lab includes 11 detectors that analyze AST to find performance issues. All 11 run on every tier — there is no detector-level gating between Starter and Pro (see Pricing Tiers).
| Category | Detector | Severity |
|---|---|---|
| Database | N+1 Query | HIGH |
| Database | Missing Index | MEDIUM |
| Performance | Inefficient Loop | MEDIUM-HIGH |
| Performance | Blocking I/O | HIGH |
| Memory | Memory Leak | CRITICAL |
| Memory | Resource Leaks | HIGH |
| Network | Large Payload | MEDIUM |
| Network | Missing Caching | MEDIUM |
| Security | ReDoS Vulnerability | HIGH |
| Frontend | Bundle Size | MEDIUM |
| Frontend | DOM Manipulation | MEDIUM |
File paths on this page refer to the backend implementation, which powers web
and API scans. The CLI and GitHub Action run a separate implementation of the
same 11 categories in packages/core-engine/src/rules/.
Detectors
N+1 Query Detector
File: backend/src/detectors/n1-query-detector.ts
What It Does: The N+1 Query Detector identifies one of the most common and devastating performance issues in database-driven applications: executing separate database queries inside loops. This pattern causes exponential growth in database load as your data scales.
Why It Matters: When you fetch a list of N items and then query the database once for each item, you execute N+1 total queries (1 for the list + N individual queries). With 100 items, this becomes 101 queries instead of 1-2 optimized queries. At scale, this can:
- Overwhelm your database with thousands of redundant queries
- Add hundreds of milliseconds to seconds of latency per request
- Cause connection pool exhaustion
- Increase database costs significantly
How It Works:
The detector performs AST traversal to identify loops (for, for-of, forEach, map, etc.) containing database operations. It recognizes query patterns from major ORMs:
- Prisma:
findUnique,findFirst,findMany - Sequelize:
findByPk,findOne,findAll - Mongoose:
findById,findOne,find - TypeORM:
findOne,findOneBy,find - Knex:
select,where, raw queries
The detector tracks query depth (nested loops increase severity) and counts multiple query calls to calculate accurate impact scores.
Detection Patterns:
for/for-of/for-in/while/forEach/maploops- Database calls:
findOne,findByPk,findUnique,findAll, etc. - ORM-aware: Prisma, Sequelize, Mongoose, TypeORM, Knex
Example Issue:
// ❌ N+1 Query - executes N database queries
async function getOrdersWithUsers() {
const orders = await Order.findAll();
for (const order of orders) {
const user = await User.findByPk(order.userId); // Query in loop!
order.user = user;
}
return orders;
}
Severity Calculation:
- Critical (≥3 queries in nested loops)
- High (≥2 queries or deeply nested)
- Medium (1 query in simple loop)
Impact Metrics:
{
category: 'performance',
potentialQueryReduction: 'N queries → 1 query',
fixDifficulty: 'moderate'
}
Inefficient Loop Detector
File: backend/src/detectors/inefficient-loop-detector.ts
What It Does: The Inefficient Loop Detector is the most comprehensive detector, analyzing 12 distinct anti-patterns that cause performance degradation in iterative code. Loops are execution hotspots—optimizing them provides immediate, measurable performance gains.
Why It Matters: Loops execute repeatedly, so even minor inefficiencies compound dramatically:
- An O(n²) nested loop with 1000 items = 1,000,000 operations
- Sequential
awaitin a 100-item loop can add 10+ seconds vs parallel execution - DOM manipulation in loops causes layout thrashing, freezing the UI
- String concatenation in loops creates thousands of intermediate strings, exhausting memory
How It Works: The detector uses multi-pass AST analysis:
- First Pass: Identifies all loop constructs and their nesting depth
- Second Pass: Scans loop bodies for problematic patterns
- Context Analysis: Checks if operations can be hoisted, parallelized, or optimized
- Framework Awareness: Recognizes React/Vue/Angular patterns to avoid false positives
It detects complexity by analyzing call graphs, tracking variable usage across loop iterations, and measuring cognitive complexity of nested structures.
Detection Patterns:
| Pattern | Severity | Description |
|---|---|---|
await in loop |
HIGH | Sequential async instead of parallel |
| Nested loops | MEDIUM-HIGH | O(n²) or O(n³) complexity |
| Array method chaining | MEDIUM | .filter().map() multiple iterations |
| Nested array methods | HIGH | O(n²) from nested .map()/.filter() |
Array.push in loop |
LOW | Inefficient array building |
| DOM manipulation | HIGH | Layout thrashing |
| String concatenation | MEDIUM | Use Array.join() instead |
| Regex compilation | MEDIUM | Compile regex outside loop |
| JSON operations | MEDIUM | Avoid JSON.parse/stringify in loop |
| Sync file I/O | CRITICAL | Blocking operations |
Array.includes/indexOf |
MEDIUM | O(n²) → use Set/Map |
Object.keys() lookups |
MEDIUM | Inefficient object iteration |
Example Issue:
// ❌ await in loop - sequential execution
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processItem(item); // Sequential!
results.push(result);
}
return results;
}
// ✅ Suggested fix - parallel execution
async function processItems(items) {
return Promise.all(items.map(item => processItem(item)));
}
Memory Leak Detector
File: backend/src/detectors/memory-leak-detector.ts
What It Does: The Memory Leak Detector identifies patterns where resources are allocated but never released, causing memory to grow unbounded over time. It’s framework-aware, understanding React, Vue, and Angular lifecycle patterns to catch cleanup violations.
Why It Matters: Memory leaks are insidious—they don’t crash immediately but degrade performance over hours or days:
- Event listeners accumulate on every component mount, consuming memory and slowing event dispatch
- Timers continue firing after components unmount, triggering state updates on destroyed components
- Global variable pollution creates permanent memory references
- In SPAs (Single Page Applications), leaks compound as users navigate, eventually freezing the browser
- Mobile devices with limited RAM crash faster from memory leaks
How It Works: The detector performs sophisticated lifecycle analysis:
- Import Detection: Scans imports to identify the framework (React
useEffect, VueonMounted, AngularOnDestroy) - Lifecycle Tracking: Maps resource allocations to their expected cleanup locations
- Cleanup Verification: Checks if allocated resources have corresponding cleanup calls
- Control Flow Analysis: Ensures cleanup happens on all code paths (not just happy path)
- Dependency Analysis: For React, validates that cleanup dependencies match effect dependencies
It tracks event listeners, timers, subscriptions, WebSocket connections, and animation frames across component lifecycles.
Detection Patterns:
| Type | Severity | Detection |
|---|---|---|
| Event listeners | HIGH | addEventListener without removeEventListener |
| Timers | HIGH | setInterval/setTimeout without cleanup |
| Global variables | MEDIUM | window.x / global.x assignments |
| Closures | MEDIUM | Closures capturing large data |
| React effects | HIGH | useEffect without cleanup |
| Vue lifecycle | HIGH | Missing unmounted cleanup |
| Angular lifecycle | HIGH | Missing ngOnDestroy cleanup |
Framework Detection:
// Detects framework from imports
import { useEffect } from 'react'; // → React context
import { onMounted } from 'vue'; // → Vue context
import { OnDestroy } from '@angular/core'; // → Angular context
Example Issue:
// ❌ React useEffect without cleanup
useEffect(() => {
const handler = () => console.log('resize');
window.addEventListener('resize', handler);
// Missing cleanup!
}, []);
// ✅ With cleanup
useEffect(() => {
const handler = () => console.log('resize');
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
Large Payload Detector
File: backend/src/detectors/large-payload-detector.ts
What It Does: The Large Payload Detector identifies database queries and API endpoints that return unbounded or excessive data. These “data bombs” consume bandwidth, overwhelm clients, and slow down your entire application stack.
Why It Matters: Returning too much data creates cascading problems:
- Network: A 50MB JSON response can take 30+ seconds on 3G mobile connections
- Memory: Large payloads consume RAM on both server and client, triggering garbage collection pauses
- Parsing: JSON parsing is CPU-intensive; large responses freeze the UI
- Database:
SELECT *fetches unused columns, wasting I/O and network bandwidth - Cost: Cloud egress charges multiply with payload size
Real example: An unbounded /users endpoint returning 50,000 users as JSON (20MB) instead of paginated results.
How It Works: The detector performs data-flow analysis:
- Query Detection: Identifies database calls (
findAll,find, raw SQL) - Pagination Check: Scans for
limit,take,skip,offsetparameters - Response Tracking: Follows variables from DB to API response (
res.json,res.send) - Column Analysis: Detects
SELECT *vs explicit field selection - Streaming Detection: Recognizes streaming responses as safe (not loaded into memory)
It understands common pagination patterns: withPagination(), cursor-based pagination, and offset/limit pagination.
Detection Patterns:
| Pattern | Severity | Description |
|---|---|---|
SELECT * queries |
MEDIUM | Fetching all columns |
| Missing pagination | HIGH | Unbounded result sets |
| No field selection | MEDIUM | Returning full objects |
| Large array returns | MEDIUM | Arrays without limits |
Data-Flow Analysis:
- Tracks variables from database call to API response
- Recognizes pagination wrappers (
paginate,withPagination) - Detects streaming responses
Example Issue:
// ❌ No pagination, returns all users
app.get('/users', async (req, res) => {
const users = await User.findAll(); // Unbounded!
res.json(users);
});
// ✅ With pagination
app.get('/users', async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const users = await User.findAll({
limit,
offset: (page - 1) * limit
});
res.json(users);
});
Missing Index Detector
File: backend/src/detectors/missing-index-detector.ts
What It Does: The Missing Index Detector analyzes database queries to identify columns that would benefit from indexing. Unindexed queries force full table scans, which become exponentially slower as data grows.
Why It Matters: Missing indexes are the #1 cause of database performance degradation in production:
- A query on an unindexed column scans every row: 1M rows = 1M checks
- With an index, the same query uses binary search: 1M rows = ~20 checks (log₂N)
- Table scans lock rows longer, increasing contention and blocking other queries
- As tables grow from 1K to 1M rows, unindexed queries slow 1000x, while indexed queries barely slow
- Production databases often have 100+ missing indexes costing 10-100ms per query
How It Works: The detector performs query analysis:
- ORM Query Parsing: Extracts WHERE conditions from Prisma
where, SequelizefindAll, Mongoosefind - Raw SQL Parsing: Analyzes SQL strings for WHERE, ORDER BY, JOIN clauses
- Column Extraction: Identifies columns used in filter/sort operations
- Index Simulation: Estimates if current query pattern would use an index
- Recommendation: Suggests composite indexes for multi-column queries
Detection Patterns:
WHEREclauses on non-indexed columnsORDER BYon large tables without covering indexJOINconditions on foreign keys- Queries combining multiple unindexed filters
Blocking I/O Detector
File: backend/src/detectors/blocking-io-detector.ts
What It Does: The Blocking I/O Detector identifies synchronous operations that freeze Node.js’s event loop, preventing the server from handling other requests. Even a single blocking operation can halt your entire application.
Why It Matters: Node.js is single-threaded—blocking operations stall everything:
- A 100ms
readFileSync()means 0 requests processed for 100ms - Under load (100 req/sec), one blocking call creates a 10-second queue
- Synchronous crypto operations (
pbkdf2Sync) can block for 500ms+ per request execSyncfor shell commands blocks until the process completes- In production, blocking I/O causes cascading timeouts and 503 errors
Real scenario: A dashboard loading config with readFileSync blocked the entire API, causing all endpoints to timeout during traffic spikes.
How It Works: The detector scans for synchronous Node.js APIs:
- File System:
fs.readFileSync,fs.writeFileSync,fs.statSync, etc. - Child Processes:
execSync,spawnSync,child_processblocking APIs - Crypto:
pbkdf2Sync,scryptSync,randomByteswithout callback - HTTP: Synchronous request libraries (rare but exists)
- Context Analysis: Flags severity as CRITICAL if found in request handlers
Detection Patterns:
fs.readFileSync/fs.writeFileSync/fs.readdirSyncexecSync/spawnSync/child_processsynchronous methodscrypto.pbkdf2Sync/crypto.scryptSync- Unawaited network calls in request handlers
ReDoS Vulnerability Detector
File: backend/src/detectors/redos-detector.ts
What It Does: The ReDoS (Regular Expression Denial of Service) Detector identifies regex patterns vulnerable to catastrophic backtracking. A malicious input can cause these regexes to run for seconds, minutes, or even hang indefinitely.
Why It Matters: ReDoS is a critical security vulnerability:
- A single malicious string can hang a regex for 10+ seconds, blocking the event loop
- Attackers exploit this by sending crafted input to public endpoints (email validation, search, etc.)
- The 2019 Cloudflare outage was caused by a ReDoS in their WAF regex
- Even innocent user input can trigger backtracking (e.g., pasting a long string)
- ReDoS bypasses rate limiting because one request is enough
How It Works: The detector performs regex complexity analysis:
- Pattern Extraction: Finds all regex literals and
new RegExp()calls - Quantifier Analysis: Identifies nested/overlapping quantifiers (
(a+)+,(a*)*,(a|a)+) - Backtracking Simulation: Estimates worst-case time complexity
- Input Tracking: Checks if regex is applied to user input (higher risk)
- Complexity Scoring: Calculates ReDoS risk score based on nesting depth
Vulnerable Patterns:
- Nested quantifiers:
(a+)+,(a*)*,(a+)* - Overlapping alternatives:
(a|a)+,(a|ab)+ - Alternation with repetition:
(a|b)+con non-matching input
Detection Patterns:
- Nested quantifiers creating exponential backtracking
- Overlapping alternatives with repetition
- User input passed to vulnerable regex
- Complex patterns without timeout protection
Bundle Size Detector
File: backend/src/detectors/bundle-size-detector.ts
What It Does: The Bundle Size Detector identifies import patterns that bloat your JavaScript bundle, increasing initial page load time and bandwidth costs. It catches common mistakes like importing entire libraries when only small utilities are needed.
Why It Matters: Bundle size directly impacts user experience and SEO:
- Every 100KB adds ~1 second load time on 3G (53% of global mobile traffic)
- Google’s Core Web Vitals penalize slow sites in search rankings
- Mobile users abandon sites that take >3 seconds to load
- Importing all of
lodash(70KB) instead oflodash/debounce(5KB) wastes 65KB - Multiple imports from the same heavy library (e.g.,
moment) get duplicated if not tree-shaken
Real example: A React app importing full lodash + moment unnecessarily added 150KB, increasing load time from 2s to 4.5s.
How It Works: The detector analyzes import statements:
- Import Pattern Detection: Identifies namespace imports (
import * as _), default imports, named imports - Package Database: Maintains known heavy packages and their tree-shakeable alternatives
- Usage Analysis: Checks if imported items are actually used in the code
- Duplication Detection: Finds multiple subpath imports from same package
- Alternative Suggestion: Recommends lighter alternatives (e.g.,
date-fnsinstead ofmoment)
Detection Patterns:
- Namespace imports of tree-shakeable libraries:
import * as _ from 'lodash' - Heavy package imports with lighter alternatives
- Unused imports
- Multiple subpath imports that could be consolidated
- Dynamic import opportunities for code-splitting
DOM Manipulation Detector
File: backend/src/detectors/dom-manipulation-detector.ts
What It Does: The DOM Manipulation Detector identifies patterns that cause layout thrashing and UI freezes. The DOM is one of the slowest APIs in browsers—inefficient manipulation creates visible jank.
Why It Matters: Browser rendering is expensive and synchronous:
- Reading layout properties (
offsetHeight,scrollTop) forces a full page layout calculation (reflow) - Alternating reads and writes causes multiple reflows: read → reflow → write → reflow → read → reflow
- A single reflow can take 10-50ms; layout thrashing can cause 100+ reflows per frame
- 60fps requires each frame to complete in 16.6ms; layout thrashing drops to <20fps
- DOM queries in loops scan the entire DOM tree repeatedly: 1000 iterations = 1000 DOM traversals
innerHTMLwith user input creates XSS vulnerabilities
How It Works: The detector tracks DOM operation patterns:
- Property Access Tracking: Identifies layout-triggering properties (
offsetWidth,clientHeight,scrollTop) - Operation Sequencing: Detects interleaved read/write patterns
- Loop Context: Flags DOM operations inside loops
- XSS Analysis: Identifies
innerHTMLwith untrusted data - querySelector Overhead: Detects repeated queries for the same selector
Detection Patterns:
- Forced synchronous layout: reading layout properties after DOM writes
- DOM manipulation in loops:
appendChild,innerHTMLin iterations - DOM queries in loops:
querySelector,getElementByIdrepeated innerHTMLwith user input (XSS risk)document.write(deprecated, blocking)
Missing Caching Detector
File: backend/src/detectors/missing-caching-detector.ts
What It Does: The Missing Caching Detector identifies expensive operations that are repeated with identical inputs—prime candidates for caching. Proper caching can reduce response times from seconds to milliseconds.
Why It Matters: Repetitive expensive operations waste resources:
- API calls to external services add 100-500ms latency + cost per request
- Complex calculations (data aggregation, ML inference) consume CPU needlessly
- Database queries for rarely-changing data hit the DB on every request
- React components re-computing the same derived state on every render
- Without memoization, a function called 1000 times does the same work 1000 times
Real scenario: A dashboard recalculating user statistics on every page load (500ms) instead of caching for 5 minutes.
How It Works: The detector performs call-site analysis:
- Expensive Operation Detection: Identifies API calls, DB queries, complex computations
- Call Frequency Analysis: Tracks if the same call happens multiple times
- Input Stability: Checks if function inputs are deterministic/stable
- Pure Function Detection: Identifies pure functions suitable for memoization
- Framework Integration: Recognizes React
useMemo, Vuecomputed, caching libraries
Detection Patterns:
- Repeated API calls with identical parameters
- Pure functions without memoization
- Database queries for static/slow-changing data
- Expensive computations in render/loop paths
- Missing HTTP cache headers on API responses
Resource Leaks Detector
File: backend/src/detectors/resource-leaks-detector.ts
What It Does: The Resource Leaks Detector identifies system resources (connections, files, streams) that are opened but never closed. These leaks exhaust connection pools and file descriptors, causing production crashes.
Why It Matters: Resource exhaustion causes catastrophic failures:
- Database connection pools have limits (typically 10-100); leaked connections cause “connection timeout” errors
- Operating systems limit open file descriptors (~1024 on Linux); exhaustion crashes the process
- Unclosed streams hold file locks, preventing other processes from accessing files
- Memory-mapped files remain in memory until explicitly closed
- In serverless (Lambda, Cloud Functions), leaked connections persist across invocations, compounding quickly
Real incident: A production API exhausted all 100 DB connections in 30 minutes due to a single unclosed connection in an error path.
How It Works: The detector performs resource lifecycle tracking:
- Acquisition Detection: Identifies resource creation (
createConnection,createReadStream,open) - Cleanup Tracking: Searches for corresponding close/release calls
- Control Flow Analysis: Ensures cleanup happens on all paths (success, error, early return)
- Try-Finally Verification: Checks if cleanup is in finally blocks
- Async Cleanup: Validates async resources are awaited before closing
Detection Patterns:
- Database connections without
.close()or.end() - File streams without
.close()or automatic cleanup - HTTP agents without connection draining
- Resources opened without try-finally protection
- Disposable objects missing cleanup
Base Detector Class
All detectors extend BaseDetector:
abstract class BaseDetector {
abstract name: string;
protected issues: Issue[] = [];
abstract detect(ast: any, context: AnalysisContext): Promise<DetectorResult>;
protected createIssue(
type: string,
severity: 'critical' | 'high' | 'medium' | 'low',
context: AnalysisContext,
lineNumber: number,
title: string,
description: string,
codeBefore: string,
codeAfter?: string,
estimatedImpact?: EstimatedImpact
): Issue;
protected createImpact(
severityScore: number,
description: string,
confidenceScore: number,
category: 'performance' | 'memory' | 'network' | 'complexity',
fixDifficulty: 'trivial' | 'easy' | 'moderate' | 'complex',
metrics: Record<string, any>
): EstimatedImpact;
protected getCode(node: any, sourceCode: string): string;
}
Creating a Custom Detector
import { BaseDetector } from './base-detector';
import { AnalysisContext, DetectorResult, Issue } from '../types';
import traverse from '@babel/traverse';
export class MyCustomDetector extends BaseDetector {
name = 'My Custom Detector';
async detect(ast: any, context: AnalysisContext): Promise<DetectorResult> {
this.reset();
const issues: Issue[] = [];
traverse(ast, {
CallExpression: (path) => {
// Your detection logic here
if (this.isProblematic(path.node)) {
issues.push(this.createIssue(
'my_custom_issue',
'medium',
context,
path.node.loc?.start.line || 0,
'Custom Issue Detected',
'Description of the issue',
this.getCode(path.node, context.sourceCode),
'// Suggested fix',
this.createImpact(50, 'Impact description', 0.8, 'performance', 'easy', {})
));
}
}
});
return { issues, detectorName: this.name };
}
private isProblematic(node: any): boolean {
// Your logic
return false;
}
}
Registering a Custom Detector
In backend/src/analyzer/code-analyzer.ts, add the detector to the record in
initializeDetectors() under a new slug:
import { MyCustomDetector } from '../detectors/my-custom-detector';
// Inside initializeDetectors()
const all: Record<string, any> = {
// ... existing detectors
'my-custom': new MyCustomDetector(),
};
Also add a matching entry to DETECTOR_REGISTRY so the slug can be selected
per scan. To attach a detector to an existing analyzer instance instead:
analyzer.addDetector(new MyCustomDetector());
Detector Configuration
Detectors can be configured via .codeevolutionrc.json. The loader also
accepts .codeevolutionrc and codeevolution.config.json, searching upward
from the working directory.
This applies to the backend engine only. The CLI has no config file support — it is configured entirely through command-line options.
Each detector is keyed by name, with its own options. Note that these config
keys (n1-query, inefficient-loop, memory-leak, large-payload) are not
the same as the detector slugs used by the scan API’s detector picker (n1,
loop, memory, payload).
{
"ignore": [
"**/test/**",
"**/fixtures/**"
],
"detectors": {
"n1-query": {
"enabled": true,
"severity": "high"
},
"memory-leak": {
"enabled": false
},
"large-payload": {
"enabled": true,
"customPatterns": ["fetchAllData", "loadEverything"]
}
},
"severity": {
"minReportLevel": "medium",
"failOn": "high"
},
"output": {
"format": "text",
"file": null
},
"dbPatterns": [
{
"orm": "custom",
"methods": ["customQuery", "fetchRecords"]
}
]
}
A config may also extends another config file, which is merged beneath it.
See backend/.codeevolutionrc.example.json for a working example.
Severity Guidelines
| Severity | Criteria |
|---|---|
| Critical | Security vulnerability, crashes, data loss |
| High | Significant performance impact, memory leaks |
| Medium | Noticeable degradation, code smell |
| Low | Minor optimization, style suggestion |