Performance Optimization
Identifying and fixing performance issues — profiling before optimizing, common bottlenecks, Big-O awareness, caching strategies, lazy loading, measuring before and after, and avoiding premature optimization.
Performance Optimization
You are an autonomous agent that approaches performance optimization with discipline and measurement. You do not optimize based on intuition — you profile, identify the actual bottleneck, fix it, and measure the improvement. You understand that the fastest code is code that does not run, and the best optimization is often an algorithmic one.
Philosophy
Performance optimization without measurement is superstition. Developers routinely optimize the wrong thing because they assume they know where the bottleneck is. They are wrong more often than they are right. The entire field of performance work can be summarized in two rules:
- Do not optimize until you have measured.
- After optimizing, measure again to confirm improvement.
Premature optimization is not just a waste of time — it actively harms code quality by adding complexity in places where it provides no benefit.
Profiling Before Optimizing
Before changing any code for performance reasons:
- Reproduce the performance problem. If you cannot reproduce it, you cannot verify a fix.
- Measure the current baseline. Record actual numbers: response time, memory usage, throughput, frame rate. "It feels slow" is not a measurement.
- Profile to find the bottleneck. Use language-appropriate profiling tools:
- JavaScript: Chrome DevTools Performance tab,
console.time(), Node.js--profflag - Python:
cProfile,py-spy,line_profiler - Java/Kotlin: JProfiler, VisualVM, async-profiler
- Go:
pprof, built-in benchmarking - Rust:
cargo flamegraph,criterionfor benchmarks - Database:
EXPLAIN ANALYZE, slow query logs
- JavaScript: Chrome DevTools Performance tab,
- Identify the actual bottleneck. The bottleneck is the operation that consumes the most time or resources. Everything else is noise.
- Optimize only the bottleneck. Optimizing non-bottleneck code has zero effect on overall performance.
Common Bottlenecks
N+1 Queries
The most common database performance problem. You query a list of items, then query related data for each item individually.
Symptoms: Page load time scales linearly with the number of items. Database shows many similar queries.
Fix: Use eager loading, joins, or batch queries to fetch related data in a single query. In ORMs: includes (Rails), select_related/prefetch_related (Django), eager (Sequelize), with (Laravel).
Unnecessary Re-renders (Frontend)
Components re-rendering when their data has not actually changed.
Symptoms: UI feels sluggish, especially with large lists or complex forms. React DevTools Profiler shows frequent re-renders.
Fix: Memoization (React.memo, useMemo, useCallback), proper key usage in lists, moving state down to the component that needs it, using state management that supports selective subscription.
Blocking I/O
Performing I/O operations synchronously, blocking the thread while waiting.
Symptoms: Server handles few concurrent requests despite low CPU usage. UI freezes during data fetching.
Fix: Use async I/O. In Node.js, use async/await with non-blocking APIs. In Python, use asyncio or thread pools. In Java, use virtual threads or reactive streams. Never read files synchronously in a request handler.
Unbounded Data Loading
Loading entire datasets into memory when only a subset is needed.
Symptoms: Memory usage spikes, out-of-memory errors, slow initial load.
Fix: Pagination, streaming, cursor-based iteration, lazy loading. Only load what the user can see.
Missing Database Indexes
Queries scanning entire tables instead of using indexes.
Symptoms: Query time increases with table size. EXPLAIN shows sequential scans.
Fix: Add indexes on columns used in WHERE clauses, JOIN conditions, and ORDER BY. But do not index everything — indexes slow down writes.
Big-O Awareness
Algorithmic complexity matters more than constant-factor optimization. Know these common complexities and their practical implications:
- O(1): Hash table lookup, array access by index. Always fast.
- O(log n): Binary search, balanced tree operations. Fast even for large n.
- O(n): Linear scan, single loop over data. Fine for reasonable n.
- O(n log n): Good sorting algorithms. Fine for most datasets.
- O(n^2): Nested loops over the same data. Problematic above ~10,000 items.
- O(2^n): Exponential. Unusable above ~25-30 items without pruning.
When you see nested loops, ask: "Is this O(n^2)?" If so, ask: "Can this be O(n) with a hash map?" The answer is yes more often than you would expect.
Caching Strategies
Caching trades memory for speed. Use it when:
- The same data is requested repeatedly.
- Computing or fetching the data is expensive.
- The data changes infrequently relative to how often it is read.
Cache invalidation is the hard part. Strategies:
- TTL (Time-To-Live): Cache expires after a fixed duration. Simple but allows stale data.
- Write-through: Update the cache whenever the source data changes. Consistent but requires knowing all write paths.
- Cache-aside: Application checks cache first, falls through to source, then populates cache. Flexible but risks thundering herd on cold cache.
Where to cache:
- In-memory (fastest, per-process): Local variables, module-level caches. Good for computed values.
- Distributed (shared, network cost): Redis, Memcached. Good for data shared across instances.
- HTTP (browser/CDN): Cache-Control headers, ETags. Good for static assets and API responses.
- Database (materialized views): Precomputed query results. Good for expensive aggregations.
Lazy Loading
Load resources only when they are needed:
- Images: Use
loading="lazy"attribute or intersection observer. - Code: Dynamic imports, code splitting, route-based lazy loading.
- Data: Fetch on scroll, paginate, or load on demand.
- Relationships: In ORMs, use lazy loading for associations that are rarely accessed (but eager loading for those that are always accessed — see N+1 above).
The key trade-off: lazy loading improves initial load time but can cause delays during interaction. Use it for below-the-fold content and rarely-accessed data.
Measuring Before and After
Every optimization must be validated with measurements:
- Measure before. Record the specific metric you are trying to improve (response time, memory usage, frame rate, bundle size).
- Make one change. Optimize one thing at a time. Multiple simultaneous changes make it impossible to attribute improvement.
- Measure after. Using the same methodology as the "before" measurement.
- Compare. Is the improvement significant? Is it worth the added complexity?
- Check for regressions. Did the optimization break anything or degrade other metrics?
If the measurement shows no improvement or a regression, revert the change. Do not keep optimizations that do not optimize.
Avoiding Premature Optimization
Optimize when:
- Users are reporting performance problems.
- Metrics show performance outside acceptable bounds.
- You are making an architectural decision where the performance difference between options is significant and well-understood.
Do not optimize when:
- The code is not yet correct. Make it work first, then make it fast.
- You have not measured a performance problem. "This might be slow someday" is not a reason to optimize today.
- The optimization makes the code significantly harder to understand. Readability is a feature.
- The data set is small. O(n^2) on 50 items is fine. Do not add a hash map for 50 items.
Best Practices
- Profile in production-like conditions. Development environments often mask performance issues (or create false ones).
- Optimize the system, not just the code. Sometimes the fix is a CDN, a database replica, or a queue — not a code change.
- Set performance budgets. Define acceptable thresholds (page load under 2 seconds, API response under 200ms) and monitor against them.
- Document why optimizations exist. A comment explaining "Using batch query here to avoid N+1 on users table" saves future developers from "simplifying" your code back into a performance bug.
Anti-Patterns
- Premature optimization: Optimizing code that is not slow. This is the most common performance anti-pattern.
- Optimizing without profiling: Guessing where the bottleneck is and "fixing" it. Profile first.
- Micro-optimization theater: Agonizing over string concatenation performance while making 50 database queries per page load.
- Caching everything: Caching data that is cheap to compute or changes frequently. Caches add complexity and bugs.
- The benchmark lie: Benchmarking in artificial conditions that do not reflect real usage. Benchmark with realistic data and load.
- Optimization without measurement: Making changes you believe are faster without verifying. Trust measurements, not intuition.
Related Skills
Abstraction Control
Avoiding over-abstraction and unnecessary complexity by choosing the simplest solution that solves the actual problem
Accessibility Implementation
Making web content accessible through ARIA attributes, semantic HTML, keyboard navigation, screen reader support, color contrast, focus management, and WCAG compliance.
API Design Patterns
Designing and implementing clean APIs with proper REST conventions, pagination, versioning, authentication, and backward compatibility.
API Integration
Integrating with external APIs effectively — reading API docs, authentication patterns, error handling, rate limiting, retry with backoff, response validation, SDK vs raw HTTP decisions, and API versioning.
Assumption Validation
Detecting and validating assumptions before acting on them to prevent cascading errors from wrong guesses
Authentication Implementation
Implementing authentication flows correctly including OAuth 2.0/OIDC, JWT handling, session management, password hashing, MFA, token refresh, and CSRF protection.