skills/development/performance-audit/SKILL.md
Use this skill when the user says 'performance audit', 'why is it slow', 'optimize performance', 'page speed', 'Core Web Vitals', 'lighthouse', 'load time', or needs to diagnose and fix frontend or backend performance issues. Do NOT use for code reviews or security audits.
npx skillsauth add cwinvestments/memstack memstack-development-performance-auditInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Identify and prioritize performance bottlenecks across frontend, backend, and network layers with measured impact and fix priority.
When this skill activates, output:
🚀 Performance Audit — Scanning for performance bottlenecks...
| Context | Status | |---------|--------| | User says "performance audit", "optimize", "slow app" | ACTIVE | | User mentions bundle size, N+1 queries, or Core Web Vitals | ACTIVE | | User wants to find memory leaks or unnecessary re-renders | ACTIVE | | User wants database schema optimization specifically | DORMANT — see database-architect | | User wants API design review (not performance) | DORMANT — see api-designer | | User wants general code quality review | DORMANT — see code-reviewer |
Ask the user for:
Check bundle size and composition:
# Next.js
npx @next/bundle-analyzer
# or: ANALYZE=true next build
# Webpack (generic)
npx webpack-bundle-analyzer stats.json
# Vite
npx vite-bundle-visualizer
What to look for:
| Issue | Detection | Impact | Fix | |-------|-----------|--------|-----| | Large dependencies | Bundle > 200KB gzipped | Slow initial load | Replace with lighter alternatives | | Duplicate packages | Same lib in multiple versions | Wasted bytes | Dedupe or pin single version | | Unused exports | Tree-shaking not working | Wasted bytes | Use ESM imports, avoid barrel files | | No code splitting | Single large bundle | Slow initial load | Dynamic imports, route-based splitting | | Unoptimized images | Images > 100KB without optimization | Slow load | next/image, sharp, WebP/AVIF | | Missing compression | No gzip/brotli on responses | 60-80% larger payloads | Enable in server/CDN config |
Common heavy dependencies and alternatives:
| Package | Size (minified) | Lighter Alternative | Size |
|---------|----------------|-------------------|------|
| moment | 72 KB | date-fns (tree-shakeable) | ~2-5 KB used |
| lodash (full) | 72 KB | lodash-es (tree-shakeable) | ~1-3 KB used |
| axios | 14 KB | fetch (native) | 0 KB |
| classnames | 1 KB | Template literals | 0 KB |
| uuid | 3 KB | crypto.randomUUID() | 0 KB |
| chart.js | 65 KB | Specific chart lib for your use case | Varies |
Tree-shaking checks:
── TREE-SHAKING ISSUES ────────────────────
[x] Barrel files (index.ts re-exporting everything)
File: src/utils/index.ts
Impact: Imports entire utils even if using one function
Fix: Import directly from source file
[x] CommonJS modules (require() instead of import)
File: src/legacy/helper.js
Impact: Cannot tree-shake CJS modules
Fix: Convert to ESM or find ESM version of dependency
[x] Side effects not declared
Package: [name]
Impact: Bundler can't safely remove unused code
Fix: Add "sideEffects": false to package.json
Audit for the three Core Web Vitals:
── CORE WEB VITALS AUDIT ──────────────────
LCP (Largest Contentful Paint) — Target: < 2.5s
Current: [measured or estimated]
Issues found:
[ ] Hero image not optimized (missing width/height, no lazy loading)
[ ] Render-blocking CSS in <head> (large stylesheet before content)
[ ] Web fonts blocking render (no font-display: swap)
[ ] Server response time > 600ms (TTFB too high)
[ ] Third-party scripts blocking main thread
FID / INP (Interaction to Next Paint) — Target: < 200ms
Current: [measured or estimated]
Issues found:
[ ] Long tasks on main thread (> 50ms blocks)
[ ] Heavy JavaScript execution on page load
[ ] Synchronous XHR calls blocking interaction
[ ] Event handlers doing expensive computation
CLS (Cumulative Layout Shift) — Target: < 0.1
Current: [measured or estimated]
Issues found:
[ ] Images without explicit dimensions (width/height)
[ ] Dynamically injected content above fold
[ ] Web fonts causing layout shift (FOUT/FOIT)
[ ] Ads or embeds without reserved space
Quick LCP fixes:
// 1. Preload hero image
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
// 2. Font display swap
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap;
}
// 3. Next.js Image component (automatic optimization)
import Image from 'next/image';
<Image src="/hero.jpg" width={1200} height={600} priority alt="Hero" />
Unnecessary re-render detection:
| Pattern | Detection | Impact | Fix |
|---------|-----------|--------|-----|
| Passing new objects/arrays as props | onClick={() => {}} in JSX | Child re-renders every parent render | Extract handler, use useCallback |
| Missing memoization on expensive computation | Computed value in render body | Recalculated every render | useMemo for expensive calculations |
| Context causing tree-wide re-renders | Context value changes frequently | All consumers re-render | Split context, use selectors |
| Missing key on list items | React DevTools warnings | Incorrect DOM reconciliation | Add stable unique key |
| Props drilling deep component trees | Props passed through 5+ levels | Tight coupling, re-render chains | Context or composition pattern |
React performance audit checklist:
── REACT PERFORMANCE ──────────────────────
Component rendering:
[ ] React.memo on pure presentational components
[ ] useCallback for handlers passed to memoized children
[ ] useMemo for expensive computed values (> 1ms)
[ ] Stable keys on list items (not array index for dynamic lists)
State management:
[ ] State colocated to where it's used (not lifted too high)
[ ] Context split: frequent-update values separate from static config
[ ] Avoid storing derived state (compute from source of truth)
[ ] Batch state updates where possible
Data fetching:
[ ] Loading states prevent waterfall fetches
[ ] Parallel fetches where data is independent
[ ] Cache API responses (React Query, SWR)
[ ] Pagination/infinite scroll for large lists
Rendering:
[ ] Virtualize long lists (react-window, @tanstack/virtual)
[ ] Lazy load below-fold components (React.lazy + Suspense)
[ ] Avoid layout thrashing (reading DOM then writing immediately)
[ ] Debounce/throttle scroll and resize handlers
N+1 query detection:
── N+1 QUERY PATTERNS ─────────────────────
Pattern: Fetching related data in a loop
Example:
const users = await db.query('SELECT * FROM users');
for (const user of users) {
user.posts = await db.query('SELECT * FROM posts WHERE user_id = ?', [user.id]);
}
Queries generated: 1 + N (where N = number of users)
Fix: JOIN or subquery
SELECT u.*, p.* FROM users u
LEFT JOIN posts p ON p.user_id = u.id;
ORM fix (Prisma):
await prisma.user.findMany({ include: { posts: true } });
ORM fix (Drizzle):
await db.query.users.findMany({ with: { posts: true } });
Missing index detection:
-- Find slow queries (PostgreSQL)
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
-- Find missing indexes
SELECT relname, seq_scan, seq_tup_read, idx_scan
FROM pg_stat_user_tables
WHERE seq_scan > 100 AND idx_scan < seq_scan / 10
ORDER BY seq_scan DESC;
-- Suggested indexes for common patterns:
-- Foreign keys: CREATE INDEX idx_posts_user_id ON posts(user_id);
-- Filtered queries: CREATE INDEX idx_users_active ON users(status) WHERE status = 'active';
-- Sorting: CREATE INDEX idx_posts_created ON posts(created_at DESC);
-- Text search: CREATE INDEX idx_users_email ON users(email);
Query optimization checklist:
| Issue | Detection | Impact | Fix | |-------|-----------|--------|-----| | N+1 queries | Loop with DB call inside | O(N) queries → O(1) | JOIN or eager loading | | Missing indexes | Full table scans on filtered columns | Slow queries as data grows | Add targeted indexes | | SELECT * | Fetching all columns | Excess data transfer | Select only needed columns | | No pagination | Returning all records | Memory + transfer overhead | LIMIT/OFFSET or cursor pagination | | Missing connection pooling | New connection per query | Connection overhead | Use pgBouncer or built-in pool | | Unoptimized JOINs | Joining on non-indexed columns | Slow JOINs | Index JOIN columns |
Over-fetching and under-fetching:
── API RESPONSE AUDIT ─────────────────────
Endpoint: GET /api/users
Response size: 45 KB per request
Fields returned: 28
Fields used by client: 8
Over-fetch ratio: 71% wasted
Fix: Add field selection
GET /api/users?fields=id,name,email,avatar
or: implement GraphQL for flexible queries
Endpoint: GET /api/dashboard
Requests made: 6 sequential API calls
Could be: 1 aggregated endpoint
Waterfall time: 1.8s
Parallel time: 0.4s (if batched)
Fix: Create /api/dashboard aggregate endpoint
or: use Promise.all() on client for parallel fetches
Missing pagination:
── PAGINATION AUDIT ───────────────────────
Endpoint: GET /api/products
Current: Returns ALL products (2,400 records)
Response size: 890 KB
Response time: 1.2s
Fix: Add cursor-based pagination
GET /api/products?cursor=abc123&limit=50
Response: { data: [...], nextCursor: "def456", hasMore: true }
Caching opportunities:
── CACHE AUDIT ────────────────────────────
Endpoint Cacheable? TTL Strategy
────────────────────────────────────────────────────────
GET /api/products Yes 5 min CDN + browser Cache-Control
GET /api/user/:id Conditional 1 min ETag / If-None-Match
GET /api/config Yes 1 hour CDN, stale-while-revalidate
POST /api/orders No — Never cache mutations
GET /api/search Maybe 30 sec Vary by query params
Missing headers:
[ ] Cache-Control not set on static assets
[ ] No ETag on API responses
[ ] No compression (gzip/brotli) on responses
Common memory leak patterns:
| Leak Type | Detection | Symptoms | Fix |
|-----------|-----------|----------|-----|
| Event listeners | Listeners added without removal | Memory grows over time | removeEventListener in cleanup |
| Unclosed DB connections | Connection pool exhaustion | "too many connections" errors | Close connections in finally/cleanup |
| Growing arrays/maps | In-memory cache without eviction | RSS grows indefinitely | Add TTL, LRU cache, or size limit |
| Unreferenced timers | setInterval without clearInterval | Timer continues after context dies | Clear timers on shutdown/cleanup |
| Unresolved promises | Promises that never settle | Memory for callbacks retained | Add timeouts to all async operations |
| Circular references | Objects referencing each other | GC can't collect (rare in V8) | Break cycles, use WeakRef |
Detection approach:
# Node.js heap snapshot
node --inspect app.js
# Open Chrome DevTools → Memory → Take heap snapshot
# Compare two snapshots to find growing objects
# Watch RSS over time
watch -n 5 'ps -o rss -p $(pgrep -f "node app.js") | tail -1'
Cleanup patterns:
// Express: cleanup on server shutdown
const server = app.listen(3000);
const connections = new Set();
server.on('connection', (conn) => {
connections.add(conn);
conn.on('close', () => connections.delete(conn));
});
process.on('SIGTERM', () => {
server.close();
for (const conn of connections) conn.destroy();
db.end(); // Close DB pool
redis.quit(); // Close Redis
clearInterval(heartbeat); // Clear timers
});
Unnecessary API calls:
── REDUNDANT REQUEST AUDIT ────────────────
Issue: Same data fetched multiple times
/api/user/me called 4 times on dashboard load
Fix: Cache response, share via context/store
Issue: Polling when WebSocket would work
/api/notifications polled every 5 seconds
Fix: WebSocket/SSE for real-time updates
Issue: No request deduplication
Search input triggers API call per keystroke
Fix: Debounce (300ms), cancel previous request (AbortController)
Issue: Large uncompressed responses
/api/export returns 2.4 MB JSON without gzip
Fix: Enable compression middleware
Compression check:
// Express
const compression = require('compression');
app.use(compression());
// Verify: response should have
// Content-Encoding: gzip (or br for brotli)
CDN and static asset optimization:
── STATIC ASSET AUDIT ─────────────────────
Asset Type Current Optimized Savings
────────────────────────────────────────────────────────
Images PNG 2.4 MB WebP 180 KB 92%
Fonts 4 font files 2 subsetted 60%
CSS 3 files, 120 KB 1 file, 45 KB 62%
JS 1.2 MB bundle 4 chunks, 280 KB 77%
Recommendations:
[ ] Serve images in WebP/AVIF with fallback
[ ] Subset fonts to used characters only
[ ] Enable HTTP/2 for parallel asset loading
[ ] Set Cache-Control: max-age=31536000 for hashed assets
[ ] Use CDN for static assets (Cloudflare, CloudFront)
Generate a scored report:
━━━ PERFORMANCE SCORECARD ━━━━━━━━━━━━━━━━
Project: [name]
Audit date: [date]
Overall score: [X/100]
── FRONTEND ───────────────────────────────
Bundle size: [X/25] [size]KB gzipped
Issues: [list]
Core Web Vitals: [X/25]
LCP: [value]s [good/needs-work/poor]
INP: [value]ms [good/needs-work/poor]
CLS: [value] [good/needs-work/poor]
React performance: [X/10]
Re-render issues: [count]
── BACKEND ────────────────────────────────
Database: [X/15]
N+1 queries: [count found]
Missing indexes: [count found]
API efficiency: [X/10]
Over-fetching: [count endpoints]
Missing cache: [count endpoints]
No pagination: [count endpoints]
Memory safety: [X/5]
Potential leaks: [count found]
── NETWORK ────────────────────────────────
Compression: [X/5] [enabled/missing]
Asset optimization: [X/5] [optimized/unoptimized]
── PRIORITIZED FIX LIST ───────────────────
Priority Issue Impact Effort Category
──────────────────────────────────────────────────────────────────────
🔴 P1 [issue description] High Low [area]
🔴 P1 [issue description] High Medium [area]
🟡 P2 [issue description] Medium Low [area]
🟡 P2 [issue description] Medium Medium [area]
🟠 P3 [issue description] Low Low [area]
🟠 P3 [issue description] Medium High [area]
Priority scoring:
Impact estimation guide:
| Optimization | Typical Impact | |-------------|---------------| | Fix N+1 queries | 50-90% reduction in query time | | Add missing indexes | 10-100x faster queries | | Enable compression | 60-80% smaller responses | | Code splitting | 30-60% faster initial load | | Image optimization | 50-90% smaller images | | Remove unused deps | 10-40% smaller bundle | | Add caching | 80-99% fewer API calls | | Virtualize lists | 90% less DOM, smooth scrolling |
tools
Use when the user says 'save diary', 'log session', 'wrapping up', or at end of a productive session.
tools
Use when the user says 'submit to marketplace', 'publish my skill', 'share this skill', 'list on marketplace', 'submit plugin', 'publish to community', or needs to submit a skill or plugin to a community marketplace via PR. Do NOT use for building skills or writing plugin code.
development
Use when the user says 'write browser tests', 'test this page', 'playwright test', 'e2e test', 'end to end test', 'browser test', 'test the UI', or needs Playwright-based browser testing for a web application. Do NOT use for unit tests, API tests, or non-browser testing.
development
Use when the user says 'teach me', 'explain as you go', 'mentor mode', 'walk me through', 'help me learn', 'explain why', 'learning mode', or wants real-time plain language narration of decisions and tradeoffs while building. Do NOT use for code review or debugging.