
Accelint Ts Performance
- 293 installs
- 21 repo stars
- Updated August 4, 2026
- gohypergiant/agent-skills
accelint-ts-performance is an agent skill that performs systematic JavaScript and TypeScript performance audits using V8 profiling patterns to fix anti-patterns, reduce allocations, and resolve deoptimization issues.
About
accelint-ts-performance is a Hypergiant Accelint skill for systematic JavaScript and TypeScript performance auditing and optimization using V8 profiling knowledge. The skill scans code for anti-patterns including O(n²) nested loops, excessive allocations, blocking async I/O, template literal waste, and V8 deoptimization from monomorphic or polymorphic inline-cache issues. Each finding includes severity, expected gains, and before/after remediation examples with caching, batching, and algorithmic complexity fixes. Developers reach for accelint-ts-performance when code is measurably slow, hot paths need profiling, or utilities called in tight loops show allocation bloat. The skill runs as a standalone audit producing structured reports or inline optimization suggestions.
- accelint-ts-performance
- Development
Accelint Ts Performance by the numbers
- 293 all-time installs (skills.sh)
- Ranked #1,357 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gohypergiant/agent-skills --skill accelint-ts-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 293 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 4, 2026 |
| Repository | gohypergiant/agent-skills ↗ |
How do you audit TypeScript performance anti-patterns?
For development and infrastructure management.
Who is it for?
TypeScript developers profiling slow hot paths who need V8-aware audits beyond micro-optimizations, especially for utilities in tight loops.
Skip if: Developers needing general TypeScript style guidance, documentation reviews, or performance work on non-JavaScript codebases.
When should I use this skill?
JavaScript or TypeScript code is measurably slow, profiling shows bottlenecks, or the developer asks to audit allocations and V8 deoptimization.
What you get
Structured performance audit reports with severity ratings, before/after code examples, and expected runtime gain estimates.
- performance audit report
- optimized code suggestions
Files
TypeScript Performance Optimization
Systematic performance optimization for JavaScript/TypeScript codebases. Combines audit workflow with expert-level optimization patterns for runtime performance.
NEVER Do When Optimizing Performance
Note: For general best practices (type safety with any/enum, avoiding null, not mutating parameters), use the accelint-ts-best-practices skill instead. This section focuses exclusively on performance-specific anti-patterns.
- NEVER assume code is cold path - Utility functions, formatters, parsers, and validators appear simple but are frequently called in loops, rendering pipelines, or real-time systems. Always audit ALL code for performance anti-patterns. Do not make assumptions about usage frequency or skip auditing based on perceived simplicity.
- NEVER apply all optimizations blindly - Performance patterns have trade-offs. Balance optimization gains against code complexity. When conducting audits, identify ALL anti-patterns through systematic analysis and report them with expected gains. Let users decide which optimizations to apply based on their specific context.
- NEVER ignore algorithmic complexity - Optimizing O(n²) code with micro-optimizations is futile. For n=1000, algorithmic fix (O(n² → O(n)) yields 1000x speedup; micro-optimizations yield 1.1-2x at best. Fix algorithm first: use Maps/Sets for O(1) lookups, eliminate nested iterations, choose appropriate data structures.
- NEVER sacrifice correctness for speed - Performance bugs are still bugs. Optimizations frequently break edge cases: off-by-one errors in manual loops, wrong behavior for empty arrays, null handling issues. Verify behavior matches before and after. Add comprehensive tests covering edge cases before optimizing—catching bugs in production costs far more than any performance gain.
- NEVER optimize code you don't own - Shared utilities, library internals, or code actively developed by others creates merge conflicts, duplicates effort, and confuses ownership. Performance changes affect all callers; coordinate with owners or defer optimization until code stabilizes.
- NEVER ignore memory vs CPU trade-offs - Caching trades memory for speed. Unbounded memoization causes memory leaks in long-running applications. A 2x CPU speedup that increases memory 10x can trigger OOM crashes or frequent GC pauses (worse than original slowness). Profile memory usage alongside CPU; set cache size limits; use WeakMap for lifecycle-bound caches.
- NEVER assume performance across environments - V8 optimizations differ between Node.js versions (v18 vs v20), browsers (Chrome vs Safari), and architectures (x64 vs ARM). An optimization yielding 3x speedup in Chrome may regress 1.5x in Safari. Profile in ALL target environments before shipping; maintain fallback implementations for environment-specific optimizations.
- NEVER chain array methods (.filter().map().reduce()) - Each method creates intermediate arrays and iterates separately. For arrays with 10k items,
.filter().map()allocates 10k + 5k items (if 50% pass filter) and iterates twice. Use singlereducepass to iterate once with zero intermediate allocations, yielding 2-5x speedup in hot paths.
- NEVER use `Array.includes()` for repeated lookups - Array.includes() is O(n) linear search. Checking 1000 items against array of 100 is O(n×m) = 100k operations. Use
Set.has()instead: O(1) lookup via hash table, reducing 100k operations to 1000 for ~100x speedup. Build Set once upfront; amortized cost is negligible.
- NEVER await before checking if you need the result -
awaitsuspends execution immediately, even if the value isn't needed. Moveawaitinto conditional branches that actually use the result. Example:const data = await fetch(url); if (condition) { use(data); }wastes I/O time when condition is false. Better:if (condition) { const data = await fetch(url); use(data); }skips fetch entirely when unneeded.
- NEVER recompute constants inside loops - Recomputing invariants wastes CPU in every iteration. For 10k iterations,
array.lengthlookup (even if cached by engine) orMath.max(a, b)runs 10k times unnecessarily. Hoist invariants outside loops:const len = array.length; for (let i = 0; i < len; i++)or curry functions to precompute constant parameters once.
- NEVER create unbounded loops or queues - Prevents runaway resource consumption from bugs or malicious input. Set explicit limits (
for (let i = 0; i < Math.min(items.length, 10000); i++)) or timeouts. Unbounded loops can freeze UI threads; unbounded queues cause OOM crashes. Fail fast with clear limits rather than degrading gracefully into unusability.
- NEVER place `try/catch` in hot paths - V8 cannot inline functions containing try-catch blocks and marks entire function as non-optimizable. Single try-catch in hot loop causes 3-5x slowdown by preventing inlining, escape analysis, and other optimizations. Validate inputs before hot paths using type guards; move try-catch outside loops to wrap entire operation; use Result types for expected errors.
Before Optimizing Performance, Ask
Apply these tests to focus optimization efforts effectively:
Impact Assessment
- Is this code actually slow? When profiling data is available, use it to inform prioritization. When unavailable, audit all code for anti-patterns.
- What percentage of runtime does this represent? When profiling data is available, flame graphs help identify the highest-impact issues. When unavailable, report all anti-patterns found.
- Raw performance matters - Audit ALL code for performance anti-patterns regardless of current usage context. Utility functions, formatters, parsers, and data transformations are frequently called in loops, rendering pipelines, or real-time systems even when they appear simple.
Correctness Verification
- Do I have tests covering this code? Performance bugs are subtle. Comprehensive tests catch regressions from optimizations. Add tests before optimizing.
- What are the edge cases? Off-by-one errors, empty arrays, null/undefined values become more likely with manual loop optimizations. Test exhaustively.
Complexity vs Benefit
- Is the algorithmic complexity optimal? O(n) → O(1) is 1000x speedup. Micro-optimizations are 1.1-2x at best. Fix algorithm first.
- Will this optimization persist? If the code changes frequently, optimization may be discarded soon. Optimize stable code first.
- What's the readability cost? Manual loops are faster but harder to maintain than
.map(). Balance performance with team velocity.
How to Use
This skill uses progressive disclosure to minimize context usage:
1. Start with the Workflow (SKILL.md)
Follow the 4-phase audit workflow below for systematic performance analysis.
2. Reference Performance Rules Overview (AGENTS.md)
Load AGENTS.md to scan compressed rule summaries organized by category.
3. Load Specific Performance Patterns as Needed
When you identify specific performance issues, load corresponding reference files for detailed ❌/✅ examples.
4. Use the Report Template (For Explicit Audit Requests)
When users explicitly request a performance audit, load the template for consistent reporting:
- assets/output-report-template.md - Structured template with guidance
Performance Optimization Workflow
Two modes of operation:
1. Audit Mode - Skill invoked directly (/accelint-ts-performance <path>) or user explicitly requests performance audit
- Generate a structured audit report using the template (Phases 1-2 only)
- Report findings for user review before implementation
- User decides which optimizations to apply
2. Implementation Mode - Skill triggers automatically during feature work
- Identify and apply optimizations directly (all 4 phases)
- No formal report needed
- Focus on fixing issues inline
Copy this checklist to track progress:
- [ ] Phase 1: Profile - Identify actual bottlenecks using profiling tools
- [ ] Phase 2: Analyze - Categorize issues by impact and optimization category
- [ ] Phase 3: Optimize - Apply performance patterns from references/
- [ ] Phase 4: Verify - Measure improvements and validate correctnessPhase 1: Profile to Identify Bottlenecks
CRITICAL: Audit ALL code for performance anti-patterns. Do not skip code based on assumptions about usage frequency. Utility functions, formatters, parsers, validators, and data transformations are frequently called in loops, rendering pipelines, or real-time systems even if their implementation appears simple.
When profiling tools are available, use them to establish baseline measurements:
- Browser: Chrome DevTools Performance tab
- Node.js:
node --prof script.js && node --prof-process isolate-*.log
Whether profiling data is available or not: Perform systematic static code analysis to identify ALL performance anti-patterns:
- O(n²) complexity (nested loops, repeated searches)
- Excessive allocations (template literals, object spreads, array methods)
- Template literal allocation when String() would suffice
- Array method chaining (.filter().map())
- Blocking async operations
- Try/catch in loops
Output: Complete list of ALL identified anti-patterns with their locations and expected performance impact. Do not filter based on "severity" or "priority" - report everything found.
When generating audit reports (when skill is invoked directly via /accelint-ts-performance <path> or user explicitly requests performance audit), use the structured template: 1. Load assets/output-report-template.md for the report structure 2. Follow the template's guidance for consistent formatting and issue grouping
Phase 2: Analyze and Categorize Issues
For EVERY issue identified in Phase 1, categorize by optimization type:
Categorize ALL issues by optimization type:
| Issue Type | Category | Expected Gain |
|---|---|---|
| Nested loops, O(n²) complexity | Algorithmic optimization | 10-1000x |
| Repeated expensive computations | Caching & memoization | 2-100x |
| Allocation-heavy code | Allocation reduction | 1.5-5x |
| Sequential access violations | Memory locality | 1.5-3x |
| Excessive I/O operations | I/O optimization | 5-50x |
| Blocking async operations | I/O optimization | 2-10x |
| Property access in loops | Caching & memoization | 1.2-2x |
Quick reference for mapping issues:
Load references/quick-reference.md for detailed issue-to-category mapping and anti-pattern detection.
Output: Categorized list of ALL issues with their optimization categories. Do not filter or prioritize - list everything found in Phase 1.
Phase 3: Optimize Using Performance Patterns
Step 1: Identify your bottleneck category from Phase 2 analysis.
Step 2: Load MANDATORY references for your category. Read each file completely with no range limits.
| Category | MANDATORY Files | Optional | Do NOT Load |
|---|---|---|---|
| Algorithmic (O(n²), nested loops, repeated lookups) | reduce-looping.md<br>reduce-branching.md | — | memoization, caching, I/O, allocation |
| Caching (property access in loops, repeated calculations) | memoization.md<br>cache-property-access.md | cache-storage-api.md (for Storage APIs) | I/O, allocation |
| I/O (blocking async, excessive I/O operations) | batching.md<br>defer-await.md | — | algorithmic, memory |
| Memory (allocation-heavy, GC pressure) | object-operations.md<br>avoid-allocations.md | — | I/O, caching |
| Locality (sequential access violations, cache misses) | predictable-execution.md | — | all others |
| Safety (unbounded loops, runaway queues) | bounded-iteration.md | — | all others |
| Micro-opt (hot path fine-tuning, 1.1-2x improvements) | currying.md<br>performance-misc.md | — | all others (apply only after algorithmic fixes) |
Notes:
- If bottleneck spans multiple categories, load references for all relevant categories
- Only apply micro-optimizations if: bottleneck is in hot path, algorithmic optimization already applied, need additional 1.1-2x performance
---
Step 3: Scan for quick reference during optimization
Load AGENTS.md to see compressed rule summaries organized by category. Use as a quick lookup while implementing patterns from the detailed reference files above.
Apply patterns systematically:
1. Load the reference file for the identified issue category 2. Scan the ❌/✅ examples to find matching patterns 3. Apply the optimization with minimal changes to preserve correctness 4. Add comments explaining the optimization and referencing the pattern
Example optimization:
// ❌ Before: O(n²) - nested iteration
for (const user of users) {
const items = allItems.filter(item => item.userId === user.id);
process(items);
}
// ✅ After: O(n) - single pass with Map lookup
// Performance: reduce-looping.md - build lookup once pattern
const itemsByUser = new Map<string, Item[]>();
for (const item of allItems) {
if (!itemsByUser.has(item.userId)) {
itemsByUser.set(item.userId, []);
}
itemsByUser.get(item.userId)!.push(item);
}
for (const user of users) {
const items = itemsByUser.get(user.id) ?? [];
process(items);
}Phase 4: Verify Improvements
Measure performance gain: 1. Re-run profiler with same inputs 2. Compare before/after runtime percentages 3. Document speedup factor (e.g., "2.3x faster")
Verify correctness: 1. Run existing test suite - all tests must pass 2. Add new tests for edge cases affected by optimization 3. Manual testing for user-facing functionality
Document optimization:
// Performance optimization applied: 2026-01-28
// Issue: Nested iteration causing O(n²) complexity with 10k items
// Pattern: reduce-looping.md - Map-based lookup
// Speedup: 145x faster (5200ms → 36ms)
// Verified: All tests pass, manual QA completeDeciding whether to keep the optimization:
- >10x speedup: Always keep if tests pass
- 2-10x speedup: Keep if tests pass and code remains maintainable
- 1.2-2x speedup: Keep for hot paths (>1000 executions/sec) or real-time systems
- 1.05-1.2x speedup: Keep only if trivial change or critical rendering/animation loop
- <1.05x speedup: Revert unless it also improves readability
Real-time systems (60fps rendering, live data visualization): Even 1.05x improvements matter in critical hot paths. Use frame timing profiler to verify impact on frame budget (16.67ms for 60fps).
If tests fail: Fix the optimization or revert. Performance bugs are still bugs.
Freedom Calibration
Calibrate guidance specificity to optimization impact:
| Optimization Type | Freedom Level | Guidance Format | Example |
|---|---|---|---|
| Algorithmic (10x+ gain) | Medium freedom | Multiple valid approaches, pick based on constraints | "Use Map for O(1) lookup or Set for deduplication" |
| Caching (2-10x gain) | Medium freedom | Pattern with examples, cache invalidation strategy | "Memoize with WeakMap if lifecycle matches source objects" |
| Micro-optimization (1.1-2x) | Low freedom | Exact pattern from reference, measure first | "Cache array.length in loop: for (let i = 0, len = arr.length; ...)" |
The test: "What's the speedup and maintenance cost?"
- 10x+ speedup → Worth complexity, medium freedom with patterns
- 2-10x speedup → Justify with measurements, medium freedom
- 1.2-2x speedup → Valuable for hot paths and real-time systems, low freedom with exact patterns
- 1.05-1.2x speedup → Only if trivial change or critical hot path (60fps rendering, etc.)
Important Notes
- Audit everything philosophy - Audit ALL code for performance anti-patterns. Utility functions, formatters, parsers, and validators are frequently called in loops or real-time systems even when they appear simple. Do not make assumptions about usage frequency.
- Report all findings - Whether profiling data is available or not, perform systematic static analysis to identify and report ALL anti-patterns with their expected gains. Do not filter based on "severity" or "priority."
- Reference files are authoritative - The patterns in references/ have been validated. Follow them exactly unless measurements prove otherwise.
- Hot path definition - Code executed >1000 times per user interaction or >100 times per second in server contexts. For real-time systems (60fps rendering, live visualization), hot paths are functions in the critical rendering loop consuming >1ms per frame.
- Real-time systems have stricter requirements - 60fps = 16.67ms frame budget. 120fps = 8.33ms. Even 1.05x improvements in hot paths are valuable. Profile with frame timing, not just total execution time.
- Regression testing - Performance optimizations frequently introduce subtle bugs in edge cases. Add tests before optimizing.
- Memory profiling matters - Some optimizations (memoization, caching) trade memory for speed. Monitor memory usage in production, especially for long-running real-time applications.
Quick Decision Tree
Use this table to rapidly identify which optimization category applies.
Audit everything: Identify ALL performance anti-patterns in the code regardless of current usage context. Report all findings with expected gains.
| If You See... | Root Cause | Optimization Category | Expected Gain |
|---|---|---|---|
Nested for loops over same data | O(n²) complexity | Algorithmic (reduce-looping) | 10-1000x |
.filter() followed by .find() or .map() | Multiple passes over data | Algorithmic (reduce-looping) | 2-10x |
Repeated array.find() or .includes() | O(n) linear search | Algorithmic (reduce-looping, use Set/Map) | 10-100x |
Many if/else chains on same variable | Branch-heavy code | Algorithmic (reduce-branching) | 1.5-3x |
| Same function called with same inputs repeatedly | Redundant computation | Caching (memoization) | 2-100x |
obj.prop.nested.deep accessed multiple times in loop | Property access overhead | Caching (cache-property-access) | 1.2-2x |
localStorage.getItem() or sessionStorage in loop | Expensive I/O in loop | Caching (cache-storage-api) | 5-20x |
Multiple await fetch() in sequence | Sequential I/O blocking | I/O (batching, defer-await) | 2-10x |
await before conditional that might not need result | Premature async suspension | I/O (defer-await) | 1.5-3x |
Many object spreads {...obj} or [...arr] | Allocation overhead | Memory (avoid-allocations) | 1.5-5x |
| Creating objects/arrays inside hot loops | GC pressure from allocations | Memory (avoid-allocations) | 2-5x |
Object.assign() or spread when mutation is safe | Unnecessary immutability cost | Memory (object-operations) | 1.5-3x |
| Accessing array elements non-sequentially | Cache locality issues | Memory Locality (predictable-execution) | 1.5-3x |
while(true) or unbounded queue growth | Runaway resource usage | Safety (bounded-iteration) | Prevents crashes |
| Function called with mostly same first N params | Repeated parameter passing | Micro-opt (currying) | 1.1-1.5x |
try/catch inside hot loop | V8 deoptimization | Micro-opt (performance-misc) | 3-5x |
String concatenation in loop with + | Quadratic string copying | Micro-opt (performance-misc) | 2-10x |
How to use this table: 1. Identify the pattern from profiler bottleneck 2. Find matching row in "If You See..." column 3. Jump to corresponding Optimization Category in Phase 3 4. Load MANDATORY reference files for that category
TypeScript Performance Optimization
Abstract
Comprehensive performance optimization guide for JavaScript and TypeScript applications, designed for AI agents and LLMs. Each rule includes one-line summaries here, with links to detailed examples in the references/ folder. Load reference files only when you need detailed implementation guidance for a specific rule.
---
How to Use This Guide
1. Start here: Scan the rule summaries to identify relevant optimizations 2. Load references as needed: Click through to detailed examples only when implementing 3. Progressive loading: Each reference file is self-contained with ❌/✅ examples
This structure minimizes context usage while providing complete implementation guidance when needed.
---
Critical Performance Anti-Patterns
NEVER do these - they appear in codebases frequently but significantly degrade performance:
- NEVER chain array methods (.filter().map().reduce()) - creates intermediate arrays and multiple iterations; use single
reducepass (2-5x faster) - NEVER use
Array.includes()for repeated lookups - O(n) linear search; useSet.has()instead for O(1) hash lookup (10-100x faster) - NEVER await before checking if you need the result - suspends execution unnecessarily; defer
awaitinto branches that actually use the value - NEVER recompute constants inside loops - wastes CPU in every iteration; hoist invariants outside loops or curry functions to precompute
- NEVER create unbounded loops or queues - prevents runaway resource consumption; set explicit limits to prevent DoS and crashes
- NEVER place
try/catchin hot paths - V8 cannot inline functions with try-catch (3-5x slowdown); validate inputs before loops
Note: For general best practices (type safety with any/enum, avoiding null, not mutating parameters), use the accelint-ts-best-practices skill instead.
See individual reference files for detailed alternatives and ✅ correct patterns.
---
Performance Optimization Categories
Before optimizing: Profile first to identify actual bottlenecks. Premature optimization wastes effort on code that doesn't impact user experience.
Design for performance from the start. Optimize slowest resources first: network >> disk >> memory >> cpu
---
1. Algorithmic Optimization (10-1000x speedup)
Fix algorithmic complexity before applying micro-optimizations. O(n²) → O(n) provides orders of magnitude improvement.
1.1 Reduce Branching
Use table lookups instead of conditionals for static values; hoist invariants out of conditionals; use early returns. View detailed examples
1.2 Reduce Looping
Combine .filter().map() into single reduce pass; use Set.has() over Array.includes() for 10-50x speedup (O(1) vs O(n)); build lookups once. View detailed examples
---
2. Caching & Memoization (2-100x speedup)
Cache expensive computations and avoid repeated work. Trade memory for CPU when computation cost justifies it.
2.1 Memoization
Hoist loop-invariant code; precompute constants at module load; memoize pure functions with limited input domain; avoid memoizing trivial computations. View detailed examples
2.2 Cache Property Access
Cache property lookups before loops; eliminate single-use aliases; avoid unnecessary destructuring; cache array.length. View detailed examples
2.3 Cache Storage API Calls
Cache localStorage, sessionStorage, and document.cookie reads in memory; avoid repeated JSON.parse on same data. View detailed examples
---
3. I/O Optimization (2-50x speedup)
Batch operations, parallelize independent work, defer blocking operations.
3.1 Batching
Batch operations to amortize costly processes, especially for I/O-bound operations (network requests, database queries, file writes). View detailed examples
3.2 Defer Await
Move await into branches where result is actually used; parallelize independent async operations; avoid blocking on I/O that could be deferred. View detailed examples
---
4. Memory & Allocation (1.5-5x speedup)
Reduce garbage collection pressure and object creation in hot paths.
4.1 Object Operations
Mutate when safe (local scope, not returned/exposed); use shallow clones when needed; preallocate object shapes for V8 hidden classes. View detailed examples
4.2 Avoid Needless Allocations
Inline simple computations; avoid intermediate variables in hot paths; reduce GC pressure by reusing objects when possible. View detailed examples
---
5. Memory Locality (1.5-3x speedup)
Write cache-friendly code with sequential access patterns.
5.1 Predictable Execution and Cache Locality
Write code with clear execution paths; use sequential memory access; group related data; prefer Array of Structs for sequential processing. View detailed examples
---
6. Safety & Bounds (DoS prevention)
Prevent runaway resource consumption and denial-of-service scenarios.
6.1 Bounded Iteration
Set limits on all loops, queues, and data structures; prevent unbounded recursion; validate iteration counts from user input. View detailed examples
---
7. Micro-optimizations (1.05-2x speedup)
Apply in hot paths only (>1000 executions/sec). Profile first to ensure impact justifies complexity.
7.1 Currying and Partial Application
Curry functions to precompute constant parameters; reduce repeated work in loops and hot paths; optimize function call overhead. View detailed examples
7.2 Additional Performance Concerns
Batch string operations; compile regex once; avoid async overhead for synchronous code; minimize closure scope; avoid try/catch in hot paths. View detailed examples
---
Quick Reference for Bottleneck Mapping
For systematic bottleneck identification and categorization, load references/quick-reference.md which provides:
- Bottleneck symptom → Category mapping
- Profiler output → Optimization category lookup
- Anti-pattern detection with concrete code examples
- Decision matrix for when to optimize
---
Optimization Impact Summary
| Category | Typical Speedup | When to Apply |
|---|---|---|
| Algorithmic (O(n²) → O(n)) | 10-1000x | Always fix first |
| Caching & Memoization | 2-100x | Repeated expensive computations |
| I/O Optimization | 2-50x | Network, disk, async operations |
| Allocation Reduction | 1.5-5x | Hot paths with GC pressure |
| Memory Locality | 1.5-3x | Large datasets, sequential processing |
| Bounded Execution | Prevents DoS | All user-controlled iterations |
| Micro-optimizations | 1.05-2x | Hot paths only, after profiling |
Priority order: Fix algorithm first, then cache, then I/O, then allocations, then micro-optimize hot paths.
---
Important Notes
- Profile before optimizing - Use Chrome DevTools (browser) or
node --prof(Node.js). Target functions consuming >5% runtime. - Hot path definition - Code executed >1000 times per user interaction or >100 times per second in server contexts.
- Real-time systems - For 60fps rendering (16.67ms frame budget), even 1.05x improvements in critical paths matter. Profile with frame timing.
- Correctness is mandatory - Run tests before and after. Performance bugs are still bugs.
- Memory vs CPU trade-offs - Caching trades memory for speed. Monitor memory usage in production.
╭──────────────────────────╮ │ accelint-ts-performance │ ╰──────────────────────────╯
<!-- Make sure to display this warning block to the user --> ┌────────────────────────────────────────────────────┐ │ ⚠️ WARNING: This skill assumes hot path first. If │ │ you know that the suggested change is only used in │ │ a cold path and won't be used in a hot path then │ │ you are free to ignore any micro-opt suggestions. │ │ You know better than the robots. When in doubt, │ │ assume hot path. │ └────────────────────────────────────────────────────┘
Report: [Target Name]
<!-- INSTRUCTIONS FOR COMPLETING THIS TEMPLATE:
1. Replace [Target Name] with the specific file/module being audited (e.g., "Color Converters", "Authentication Service")
2. EXECUTIVE SUMMARY: Provide a high-level overview
- Summarize what was audited and the workflow used
- Count issues by category (e.g., "4 allocation anti-patterns, 5 array method inefficiencies")
- Include Impact Assessment explaining WHY optimizations matter for this specific code (infer from context)
3. PHASE 1 - ISSUE GROUPING RULES:
- Group issues when they share the SAME root cause AND same fix pattern
- Example: Multiple instances of
.every()with inline closures → group together - Example: Different allocation patterns → separate issues
- Use subsections (4-8) for grouped issues, individual numbers (1, 2, 3) for unique issues
4. PHASE 1 - EACH ISSUE/GROUP MUST INCLUDE:
- Location (file:line or file:line-range)
- Current code with ❌ marker
- Clear explanation of the issue
- Expected Gain (use ranges from skill categories - see below)
- Category (Algorithmic, Caching, I/O, Memory, Locality, Safety, Micro-opt)
- Pattern Reference (which references/*.md file)
- Recommended Fix with ✅ marker
5. EXPECTED GAIN RANGES BY CATEGORY:
- Algorithmic (O(n²) → O(n)): 10-1000x
- Caching & Memoization: 2-100x
- I/O Optimization: 2-50x
- Allocation Reduction (Memory): 1.5-5x
- Memory Locality: 1.5-3x
- Safety (Bounded Iteration): Prevents DoS/crashes
- Micro-optimizations: 1.05-2x
6. PHASE 2: Generate summary table from Phase 1 findings
- Include all issues with their numbers
- Keep it concise - one row per issue/group
See assets/audit-report-example.md for a real-world example. -->
Executive Summary
Completed systematic audit of [file/module path] following accelint-ts-performance workflow. Identified [N] performance anti-patterns with expected gains ranging from [min]x to [max]x. [Brief description of what this code does and why performance matters].
Key Findings:
- [N] [category] anti-patterns ([gain range] potential gain each)
- [N] [category] issues ([gain range] each)
- [N] [other category] opportunities ([gain range])
Impact Assessment: [Explain WHY these optimizations matter for this specific code. Consider:]
- Where is this code likely called? (hot paths, rendering loops, batch processing, etc.)
- What operations trigger it? (user interactions, real-time updates, data processing)
- Why do even small gains matter? (frame budgets, throughput requirements, scale)
---
Phase 1: Identified Anti-Patterns
1. [Function/Location] - [Issue Type]
Location: [file:line] or [file:line-range]
// ❌ Current: [Brief description of problem]
[code snippet showing the issue]Issue:
- [Point 1 explaining the problem]
- [Point 2 with specifics about allocations/iterations/complexity]
- [Point 3 quantifying the impact if possible]
Expected Gain: [range]x Category: [Algorithmic|Caching|I/O|Memory|Locality|Safety|Micro-opt] Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[code snippet showing the fix]---
2. [Function/Location] - [Issue Type]
Location: [file:line] or [file:line-range]
// ❌ Current: [Brief description of problem]
[code snippet]Issue:
- [Explanation]
Expected Gain: [range]x Category: [Category Name] Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[code snippet]---
3-N. [Grouped Issues] - [Shared Issue Type] ([N] instances)
<!-- Use this format when multiple issues share the same root cause and fix pattern -->
Locations:
[file:line]- [function/context][file:line]- [function/context][file:line]- [function/context]
Example from [specific location]:
// ❌ Current: [Brief description of problem]
[representative code snippet]Issue:
- [Shared root cause explanation]
- [Why this pattern is problematic]
- [Impact across all instances]
Expected Gain: [range]x each Category: [Category Name] Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[fixed code snippet]Same pattern applies to all [N] instances:
// [Location/function 2]
// ❌ Current
[code snippet]
// ✅ Better
[fixed snippet]
// [Location/function 3]
// ❌ Current
[code snippet]
// ✅ Better
[fixed snippet]---
Phase 2: Categorized Issues
| # | Location | Issue | Category | Expected Gain |
|---|---|---|---|---|
| 1 | [file:line] | [Brief issue description] | [Category] | [range]x |
| 2 | [file:line] | [Brief issue description] | [Category] | [range]x |
| 3 | [file:line] | [Brief issue description] | [Category] | [range]x |
| 4-N | [multiple] | [Brief issue description] | [Category] | [range]x |
Total Issues: [N] Primary Categories: [Category1] ([N]), [Category2] ([N]), [Category3] ([N])
TypeScript Performance Optimization
Systematic performance optimization for JavaScript/TypeScript codebases. Combines audit workflow with expert-level optimization patterns for runtime performance.
Overview
This skill provides:
- 4-phase workflow (Profile → Analyze → Optimize → Verify) for systematic performance auditing
- Expert optimization patterns with ❌/✅ examples for all performance categories
- Bottleneck categorization and decision frameworks for when to optimize
- Profiling tool guidance (Chrome DevTools, Node.js --prof)
When to Use
Use this skill when:
- Auditing code for performance bottlenecks
- Optimizing loops, caching, or allocation patterns
- Profiling slow code paths
- Fixing algorithmic complexity issues (O(n²) → O(n))
- Users say "optimize performance", "this is slow", "why is this slow", "reduce allocations"
Structure
accelint-ts-performance/
├── SKILL.md # 4-phase workflow + guidance
├── AGENTS.md # Compressed rule overview
├── README.md # This file
└── references/
├── quick-reference.md # Bottleneck → category mapping
├── reduce-branching.md # Convert conditionals to lookups
├── reduce-looping.md # Single-pass operations, O(1) lookups
├── memoization.md # Hoist invariants, cache results
├── cache-property-access.md # Cache lookups, eliminate aliases
├── cache-storage-api.md # Cache localStorage/sessionStorage
├── batching.md # Batch I/O operations
├── defer-await.md # Defer awaits, parallelize async
├── object-operations.md # Safe mutation, shallow clones
├── avoid-allocations.md # Inline ops, reduce GC pressure
├── predictable-execution.md # Sequential access, cache locality
├── bounded-iteration.md # Set limits on loops and queues
├── currying.md # Precompute constant parameters
└── performance-misc.md # Strings, regex, closures, try/catchProgressive Disclosure
This skill minimizes context usage through progressive loading:
1. Start with SKILL.md - Follow the 4-phase workflow 2. Load AGENTS.md - Scan compressed rule summaries 3. Load specific references - Detailed ❌/✅ examples when implementing
Performance Categories
| Category | Typical Speedup | Reference Files |
|---|---|---|
| Algorithmic optimization | 10-1000x | reduce-branching.md, reduce-looping.md |
| Caching & memoization | 2-100x | memoization.md, cache-property-access.md, cache-storage-api.md |
| I/O optimization | 2-50x | batching.md, defer-await.md |
| Allocation reduction | 1.5-5x | object-operations.md, avoid-allocations.md |
| Memory locality | 1.5-3x | predictable-execution.md |
| Safety & bounds | DoS prevention | bounded-iteration.md |
| Micro-optimizations | 1.05-2x | currying.md, performance-misc.md |
Quick Start
1. Profile first - Use Chrome DevTools or node --prof to identify bottlenecks consuming >5% runtime 2. Categorize issues - Map bottlenecks to optimization categories (see quick-reference.md) 3. Load relevant pattern - Open corresponding reference file for ❌/✅ examples 4. Apply and verify - Implement optimization, measure speedup, validate correctness with tests
Critical Anti-Patterns
NEVER do these:
- ❌ Chain array methods (
.filter().map()) - use singlereducepass - ❌ Use
Array.includes()for repeated lookups - useSet.has()(O(n) → O(1)) - ❌ Await before checking if needed - defer
awaitinto branches - ❌ Recompute constants in loops - hoist invariants outside
- ❌ Create unbounded loops - set explicit limits
- ❌ Place
try/catchin hot paths - degrades V8 optimization
See reference files for ✅ correct patterns.
License
Apache-2.0
4.12 Avoid Needless Allocations
Issues
- Intermediate variables that add allocation overhead
- Unnecessary variable assignments in hot paths
- Creating objects/arrays when inline computation suffices
- GC pressure from avoidable allocations
- Trading readability for performance in cold paths
Optimizations
- Inline simple computations instead of storing in variables
- Avoid intermediate variables in hot paths and tight loops
- Balance readability vs performance based on call frequency
- Use variables for complex expressions or multiple uses
- Reserve intermediate variables for semantic value, not simple math
Examples
Inline Simple Computations
❌ Incorrect: needless allocation
function randomInt(min: number, max: number): number {
const minCeil = Math.ceil(min);
const maxFloor = Math.floor(max);
const range = maxFloor - minCeil + 1;
return Math.floor(Math.random() * range + minCeil);
}✅ Correct: inline computation
function randomInt(min: number, max: number): number {
const minCeil = Math.ceil(min);
const maxFloor = Math.floor(max);
return Math.floor(Math.random() * (maxFloor - minCeil + 1) + minCeil);
}The range variable creates an allocation that provides no semantic value. When called frequently, these allocations create GC pressure.
Avoid Allocations in Loops
❌ Incorrect: allocate per iteration
for (let i = 0; i < items.length; i++) {
const scaledIndex = i * scaleFactor;
const offset = baseOffset + padding;
process(items[scaledIndex + offset]);
}✅ Correct: compute inline or hoist
const offset = baseOffset + padding;
for (let i = 0; i < items.length; i++) {
process(items[i * scaleFactor + offset]);
}If offset is loop-invariant, hoist it. If scaledIndex is only used once, compute inline.
When to Use Variables
✅ Good: complex expression used multiple times
function calculatePrice(quantity: number, basePrice: number): number {
const discountedPrice = basePrice * (1 - getDiscount(quantity));
return discountedPrice * quantity + getTax(discountedPrice);
}Variables are warranted when:
- Expression is complex and inlining hurts readability
- Value is used multiple times
- Expression has side effects that shouldn't repeat
- Debugging would benefit from named intermediate values
✅ Good: semantic value in business logic
function validateOrder(order: Order): boolean {
const hasValidItems = order.items.length > 0;
const isWithinLimit = order.total <= order.user.creditLimit;
const hasShippingAddress = !!order.shippingAddress;
return hasValidItems && isWithinLimit && hasShippingAddress;
}These variables add semantic clarity to business logic. The readability benefit outweighs the allocation cost in validation code.
Inline vs Variable Trade-offs
❌ Incorrect: variable for trivial computation
function distance(x1: number, y1: number, x2: number, y2: number): number {
const dx = x2 - x1;
const dy = y2 - y1;
const dxSquared = dx * dx;
const dySquared = dy * dy;
const sum = dxSquared + dySquared;
return Math.sqrt(sum);
}✅ Correct: inline simple math
function distance(x1: number, y1: number, x2: number, y2: number): number {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}Use variables for dx and dy (reused), but inline dx * dx and dy * dy (simple, single-use).
Object/Array Allocations in Hot Paths
❌ Incorrect: create objects in loop
function processPoints(points: Point[]): number {
let sum = 0;
for (const point of points) {
const normalized = { x: point.x / 100, y: point.y / 100 };
sum += normalized.x + normalized.y;
}
return sum;
}✅ Correct: inline computation
function processPoints(points: Point[]): number {
let sum = 0;
for (const point of points) {
sum += point.x / 100 + point.y / 100;
}
return sum;
}Avoid creating objects when you can compute inline. The normalized object creates allocation overhead.
Temporary Arrays
❌ Incorrect: intermediate array
function firstThreeValid(items: Item[]): Item[] {
const validItems = items.filter(isValid);
return validItems.slice(0, 3);
}✅ Correct: single pass
function firstThreeValid(items: Item[]): Item[] {
const result: Item[] = [];
for (const item of items) {
if (isValid(item)) {
result.push(item);
if (result.length === 3) break;
}
}
return result;
}Avoids allocating the full validItems array when you only need three elements.
Guidelines
- Hot paths: Prefer inline computation to minimize allocations
- Cold paths: Prefer named variables for readability
- Frequency matters: Profile to identify hot paths before optimizing
- Semantic value: Use variables when they clarify intent
- Multiple uses: Always use variables for repeated expressions
- Balance: Don't sacrifice all readability for micro-optimizations
When to Optimize
Optimize allocations when:
- Function is called thousands+ times per second
- Profiling shows GC pressure
- Running in memory-constrained environments
- Inside tight loops or hot paths
Don't optimize when:
- Function is called infrequently
- Readability significantly suffers
- No measurable performance impact
- Premature optimization in cold paths
4.4 Batching
Batch operations to amortize costly processes, especially for I/O-bound operations.
4.6 Bounded Iteration
NEVER create unbounded loops, queues, or recursive calls. Always set explicit limits to prevent runaway resource consumption.
Loop Limits
❌ Incorrect: unbounded while loop
while (true) {
if (queue.isEmpty()) break;
process(queue.pop());
}
// Risk: If queue.isEmpty() never returns true, infinite loop✅ Correct: bounded iteration with max iterations
const MAX_ITERATIONS = 10000;
let iterations = 0;
while (!queue.isEmpty() && iterations < MAX_ITERATIONS) {
process(queue.pop());
iterations++;
}
if (iterations >= MAX_ITERATIONS) {
throw new Error(`Loop exceeded ${MAX_ITERATIONS} iterations - possible infinite loop`);
}Why this matters: Production systems need fail-safes. A condition that "should always become true" may fail due to bugs, corrupted state, or edge cases. Explicit iteration limits prevent infinite loops from consuming CPU and hanging the application.
Queue Limits
❌ Incorrect: unbounded queue
const queue: Task[] = [];
function addTask(task: Task): void {
queue.push(task); // No size limit
}
// Risk: Memory exhaustion if tasks are added faster than processed✅ Correct: bounded queue with max size
const MAX_QUEUE_SIZE = 1000;
class BoundedQueue<T> {
private items: T[] = [];
constructor(private readonly maxSize: number = MAX_QUEUE_SIZE) {}
push(item: T): void {
if (this.items.length >= this.maxSize) {
throw new Error(`Queue exceeded max size ${this.maxSize}`);
}
this.items.push(item);
}
pop(): T | undefined {
return this.items.shift();
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
const queue = new BoundedQueue<Task>(1000);Why this matters: Unbounded queues can grow indefinitely if producers outpace consumers. This leads to memory exhaustion and crashes. Bounded queues fail fast with clear error messages instead of silent resource depletion.
Recursion Limits
❌ Incorrect: unbounded recursion
function traverse(node: Node): void {
if (!node) return;
process(node);
traverse(node.left);
traverse(node.right);
}
// Risk: Stack overflow on deep/cyclic structures✅ Correct: bounded recursion depth
const MAX_DEPTH = 100;
function traverse(node: Node, depth = 0): void {
if (!node) return;
if (depth >= MAX_DEPTH) {
throw new Error(`Recursion exceeded ${MAX_DEPTH} levels - possible cycle or excessive depth`);
}
process(node);
traverse(node.left, depth + 1);
traverse(node.right, depth + 1);
}Why this matters: Stack overflow crashes are hard to debug. Explicit depth limits catch cycles early and provide clear error messages. For legitimate deep structures, iterative solutions with explicit stacks are safer.
Timeout Patterns
❌ Incorrect: no timeout for long operations
async function processAll(items: Item[]): Promise<void> {
for (const item of items) {
await processItem(item); // Could run forever
}
}✅ Correct: timeout for entire operation
const TIMEOUT_MS = 30000;
async function processAll(items: Item[]): Promise<void> {
const startTime = Date.now();
for (const item of items) {
const elapsed = Date.now() - startTime;
if (elapsed > TIMEOUT_MS) {
throw new Error(`Operation exceeded ${TIMEOUT_MS}ms timeout after processing ${items.indexOf(item)} items`);
}
await processItem(item);
}
}✅ Correct alternative: per-item timeout with AbortController
const ITEM_TIMEOUT_MS = 5000;
async function processWithTimeout(item: Item): Promise<void> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ITEM_TIMEOUT_MS);
try {
await processItem(item, { signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
async function processAll(items: Item[]): Promise<void> {
for (const item of items) {
await processWithTimeout(item);
}
}Why this matters: Hung async operations can block application flow indefinitely. Timeouts ensure operations fail fast with actionable error messages rather than appearing frozen to users.
Array Length Limits
❌ Incorrect: unbounded array growth in loop
const results: Result[] = [];
for (const item of items) {
const processed = processItem(item);
results.push(...processed); // processed could contain thousands of items
}✅ Correct: check array size before adding
const MAX_RESULTS = 10000;
const results: Result[] = [];
for (const item of items) {
const processed = processItem(item);
if (results.length + processed.length > MAX_RESULTS) {
throw new Error(`Results exceeded ${MAX_RESULTS} items - possible memory issue`);
}
results.push(...processed);
}Recommended Limits
| Operation Type | Recommended Limit | Rationale |
|---|---|---|
| Loop iterations | 10,000 - 100,000 | Prevents infinite loops while allowing large datasets |
| Queue size | 1,000 - 10,000 | Prevents memory exhaustion from unbounded growth |
| Recursion depth | 100 - 1,000 | Prevents stack overflow; typical stack supports ~10k frames |
| Operation timeout | 30s - 5min | Prevents hung operations; depends on expected duration |
| Array length | 10,000 - 1,000,000 | Depends on element size and available memory |
| String length | 1MB - 100MB | Prevents memory issues from pathological inputs |
Adjust limits based on:
- Available system resources (memory, CPU)
- Expected data sizes in production
- Performance requirements
- Error recovery strategy
When to Use Each Pattern
| Scenario | Pattern | Example |
|---|---|---|
Any while(true) or do-while loop | Loop iteration counter | Processing queue until empty |
| Task queues, event queues, buffers | Queue size limit | Job processing system |
| Tree traversal, graph algorithms | Recursion depth limit | JSON parsing, DOM traversal |
| Async operations, network calls | Timeout | API calls, file I/O |
| Arrays built in loops | Array length check | Aggregating results |
| String concatenation in loops | String length check | Building large text output |
Production Considerations
Make limits configurable:
interface BoundedLoopConfig {
maxIterations?: number;
timeout?: number;
onLimitExceeded?: (reason: string) => void;
}
async function processWithLimits(
items: Item[],
config: BoundedLoopConfig = {}
): Promise<void> {
const maxIterations = config.maxIterations ?? 10000;
const timeout = config.timeout ?? 30000;
const startTime = Date.now();
for (let i = 0; i < items.length && i < maxIterations; i++) {
if (Date.now() - startTime > timeout) {
const reason = `Timeout after ${timeout}ms`;
config.onLimitExceeded?.(reason);
throw new Error(reason);
}
await processItem(items[i]);
}
}Log when approaching limits:
const MAX_ITERATIONS = 10000;
const WARN_THRESHOLD = 0.8; // Warn at 80%
let iterations = 0;
while (condition && iterations < MAX_ITERATIONS) {
iterations++;
if (iterations === Math.floor(MAX_ITERATIONS * WARN_THRESHOLD)) {
console.warn(`Loop approaching limit: ${iterations}/${MAX_ITERATIONS} iterations`);
}
process();
}Graceful degradation:
// Instead of throwing, return partial results
function processUpToLimit(items: Item[], limit = 10000): ProcessResult {
const results: Result[] = [];
const processed = Math.min(items.length, limit);
for (let i = 0; i < processed; i++) {
results.push(processItem(items[i]));
}
return {
results,
processed,
total: items.length,
truncated: items.length > limit,
};
}4.8 Cache Property Access and Variable Aliases
Issues
- Unnecessary destructuring assignments
- Repeated property access that could be cached
- Intermediate variables with no semantic value
- Excessive parameter spreading
- Multiple property lookups in hot paths
Optimizations
- Cache frequently accessed properties once
- Eliminate single-use aliases
- Direct property access when clearer
- Avoid destructuring for single property access
Examples
Cache Property Access in Loops
❌ Incorrect: 3 lookups × N iterations
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value);
}✅ Correct: 1 lookup total
const value = obj.config.settings.value;
const len = arr.length;
for (let i = 0; i < len; i++) {
process(value);
}Eliminate Single-Use Aliases
❌ Incorrect: unnecessary intermediate
function getUserName(user) {
const name = user.name;
return name;
}✅ Correct: direct access
function getUserName(user) {
return user.name;
}Unnecessary Destructuring
❌ Incorrect: destructure for single property
function process(data) {
const { id } = data;
return fetchUser(id);
}✅ Correct: direct access
function process(data) {
return fetchUser(data.id);
}Cache Repeated Property Access
❌ Incorrect: multiple lookups
function calculate(obj) {
if (obj.config.settings.enabled) {
return obj.config.settings.value * obj.config.settings.multiplier;
}
return obj.config.settings.default;
}✅ Correct: cache once
function calculate(obj) {
const settings = obj.config.settings;
if (settings.enabled) {
return settings.value * settings.multiplier;
}
return settings.default;
}Excessive Parameter Spreading
❌ Incorrect: spread entire object
function render({ id, name, email, address, phone, ...rest }) {
return formatUser(id, name);
}✅ Correct: access needed properties
function render(user) {
return formatUser(user.id, user.name);
}4.9 Cache Storage API Calls
localStorage, sessionStorage, and document.cookie are synchronous and expensive. Cache reads in memory.
❌ Incorrect: reads storage on every call
function getTheme() {
return localStorage.getItem('theme') ?? 'light';
}
// Called 10 times = 10 storage reads✅ Correct: `Map` cache
const storageCache = new Map<string, string | null>()
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key));
}
return storageCache.get(key);
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value);
storageCache.set(key, value); // keep cache in sync
}Cookie caching:
let cookieCache: Record<string, string> | null = null
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(
document.cookie.split('; ').map(c => c.split('='));
)
}
return cookieCache[name];
}Important: invalidate on external changes
window.addEventListener('storage', (e) => {
if (e.key) {
storageCache.delete(e.key);
}
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
storageCache.clear()
}
});If storage can change externally (another tab, server-set cookies), invalidate cache:
4.13 Currying and Partial Application for Performance
Overview
Convert functions to curried form when parameters are constant across many calls. Precompute expensive operations (exponentiation, regex compilation, lookups) and cache them in closures to eliminate repeated work in loops and hot paths.
Examples
Currying for Expensive Computation
❌ Incorrect: recompute multiplier every call
export function round(precision: number, value: number): number {
if (!Number.isInteger(precision)) {
throw new Error('Precision must be an integer.');
}
const multiplier = 10 ** precision;
return Math.round(value * multiplier) / multiplier;
}
// In hot path
for (const price of prices) {
rounded.push(round(2, price)); // Recomputes 10 ** 2 every iteration
}✅ Correct: curry to precompute multiplier
export function round(precision: number): (value: number) => number;
export function round(precision: number, value: number): number;
export function round(
precision: number,
value?: number,
): number | ((value: number) => number) {
if (!Number.isInteger(precision)) {
throw new Error('Precision must be an integer.');
}
const multiplier = 10 ** precision;
if (value === undefined) {
// Return curried function with precomputed multiplier
return (v: number) => Math.round(v * multiplier) / multiplier;
}
return Math.round(value * multiplier) / multiplier;
}
// In hot path
const roundTo2 = round(2); // Compute 10 ** 2 once
for (const price of prices) {
rounded.push(roundTo2(price)); // Reuse precomputed multiplier
}The curried version computes 10 ** precision once and captures it in the closure, avoiding repeated exponentiation.
Currying for Regex Compilation
❌ Incorrect: recompile regex every call
function validate(pattern: string, value: string): boolean {
const regex = new RegExp(pattern);
return regex.test(value);
}
for (const email of emails) {
if (validate('^[a-z]+@[a-z]+\\.[a-z]+$', email)) {
validEmails.push(email);
}
}✅ Correct: curry to compile regex once
function validate(pattern: string): (value: string) => boolean;
function validate(pattern: string, value: string): boolean;
function validate(
pattern: string,
value?: string,
): boolean | ((value: string) => boolean) {
const regex = new RegExp(pattern);
if (value === undefined) {
return (v: string) => regex.test(v);
}
return regex.test(value);
}
const isValidEmail = validate('^[a-z]+@[a-z]+\\.[a-z]+$');
for (const email of emails) {
if (isValidEmail(email)) {
validEmails.push(email);
}
}Currying for Configuration
❌ Incorrect: pass same config repeatedly
function formatCurrency(config: FormatConfig, amount: number): string {
const symbol = config.currencySymbol;
const decimals = config.decimalPlaces;
const multiplier = 10 ** decimals;
return symbol + (Math.round(amount * multiplier) / multiplier).toFixed(decimals);
}
// Called thousands of times with same config
for (const transaction of transactions) {
display(formatCurrency(usdConfig, transaction.amount));
}✅ Correct: curry to capture config
function formatCurrency(config: FormatConfig): (amount: number) => string;
function formatCurrency(config: FormatConfig, amount: number): string;
function formatCurrency(
config: FormatConfig,
amount?: number,
): string | ((amount: number) => string) {
const symbol = config.currencySymbol;
const decimals = config.decimalPlaces;
const multiplier = 10 ** decimals;
const format = (amt: number) =>
symbol + (Math.round(amt * multiplier) / multiplier).toFixed(decimals);
if (amount === undefined) {
return format;
}
return format(amount);
}
const formatUSD = formatCurrency(usdConfig);
for (const transaction of transactions) {
display(formatUSD(transaction.amount));
}Partial Application with bind
❌ Incorrect: repeated identical calls
function scale(factor: number, base: number, value: number): number {
return base + value * factor;
}
for (const measurement of measurements) {
scaled.push(scale(2.5, 100, measurement));
}✅ Correct: use bind for partial application
function scale(factor: number, base: number, value: number): number {
return base + value * factor;
}
const scaleMeasurement = scale.bind(null, 2.5, 100);
for (const measurement of measurements) {
scaled.push(scaleMeasurement(measurement));
}Note: bind() has overhead. For hot paths, prefer explicit currying as shown in previous examples.
Currying for Validation Rules
❌ Incorrect: recreate validators
function validateRange(min: number, max: number, value: number): boolean {
return value >= min && value <= max;
}
for (const score of scores) {
if (!validateRange(0, 100, score)) {
errors.push(`Invalid score: ${score}`);
}
}✅ Correct: curry to create reusable validator
function validateRange(min: number, max: number): (value: number) => boolean;
function validateRange(min: number, max: number, value: number): boolean;
function validateRange(
min: number,
max: number,
value?: number,
): boolean | ((value: number) => boolean) {
const check = (v: number) => v >= min && v <= max;
if (value === undefined) {
return check;
}
return check(value);
}
const isValidScore = validateRange(0, 100);
for (const score of scores) {
if (!isValidScore(score)) {
errors.push(`Invalid score: ${score}`);
}
}When NOT to Curry
✅ Good: parameters vary frequently
function add(a: number, b: number): number {
return a + b;
}
// Both parameters change every call
for (let i = 0; i < items.length; i++) {
totals[i] = add(items[i].price, items[i].tax);
}Don't curry when:
- Parameters vary on every call
- Function is called infrequently
- Setup cost is negligible
- Currying adds more overhead than it saves
Currying with TypeScript Generics
❌ Incorrect: lose type information
function map<T, U>(fn: (item: T) => U, items: T[]): U[] {
return items.map(fn);
}
// Have to pass fn and items together
const doubled = map((x: number) => x * 2, [1, 2, 3]);✅ Correct: curry with generics
function map<T, U>(fn: (item: T) => U): (items: T[]) => U[];
function map<T, U>(fn: (item: T) => U, items: T[]): U[];
function map<T, U>(
fn: (item: T) => U,
items?: T[],
): U[] | ((items: T[]) => U[]) {
const mapper = (arr: T[]) => arr.map(fn);
if (items === undefined) {
return mapper;
}
return mapper(items);
}
// Create reusable mapper
const double = map((x: number) => x * 2);
const doubled1 = double([1, 2, 3]);
const doubled2 = double([4, 5, 6]);Guidelines
- Profile first: Measure to confirm the parameter is expensive to compute
- Constant parameters: Curry when some parameters are constant across many calls
- Hot paths: Prioritize currying in loops, event handlers, and frequently-called functions
- Expensive setup: Curry functions with expensive validation, computation, or object creation
- TypeScript: Use function overloads to support both curried and direct-call APIs
- Closure cost: Be aware curried functions capture variables in closure (minimal overhead)
When to Use Currying
Curry functions when:
- Parameters include expensive computations (exponentiation, regex, lookup tables)
- Some parameters are constant across hundreds/thousands of calls
- Function is in a hot path (loop, render, event handler)
- Setup/validation cost is significant
- Creating specialized versions improves API ergonomics
When NOT to Use Currying
Avoid currying when:
- All parameters vary on every call
- Function is called infrequently
- Setup cost is trivial (simple primitives)
- Currying complexity outweighs performance gain
- Premature optimization in cold paths
Fallback Patterns
When currying doesn't work, use these alternatives:
Fallback 1: Direct function calls when parameters vary
Scenario: All parameters change on every call
// ❌ Don't curry - no benefit
const add = (a: number) => (b: number) => a + b;
for (let i = 0; i < items.length; i++) {
totals[i] = add(items[i].price)(items[i].tax); // Awkward and slow
}✅ Use direct function call
function add(a: number, b: number): number {
return a + b;
}
for (let i = 0; i < items.length; i++) {
totals[i] = add(items[i].price, items[i].tax); // Clear and fast
}Why: Currying adds closure overhead (function creation, variable capture) with zero benefit when all parameters vary.
Fallback 2: Inline computation when setup cost is trivial
Scenario: Operation is so simple that currying adds complexity
// ❌ Over-engineered
const multiply = (factor: number) => (value: number) => factor * value;
const double = multiply(2);
for (const x of values) {
results.push(double(x));
}✅ Inline the trivial operation
for (const x of values) {
results.push(x * 2); // No abstraction needed
}Why: For trivial operations (multiplication, addition), the overhead of function calls exceeds any benefit. Inline when the operation is simpler than the abstraction.
Fallback 3: Memoization for expensive pure functions
Scenario: Function is expensive but parameters vary frequently
// ❌ Currying doesn't help - parameters vary
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Called with different values each time
for (const num of numbers) {
results.push(fibonacci(num)); // O(2^n) each call
}✅ Use memoization instead
const fibCache = new Map<number, number>();
function fibonacci(n: number): number {
if (n <= 1) return n;
if (fibCache.has(n)) return fibCache.get(n)!;
const result = fibonacci(n - 1) + fibonacci(n - 2);
fibCache.set(n, result);
return result;
}
for (const num of numbers) {
results.push(fibonacci(num)); // O(n) total with cache
}Why: When parameters vary but repeat, memoization is more effective than currying. Cache stores results by input rather than precomputing constants.
Fallback 4: Loop hoisting for simple invariants
Scenario: Simple invariant doesn't need currying
// ❌ Overkill for simple hoisting
const addTax = (rate: number) => (amount: number) => amount * (1 + rate);
const withTax = addTax(0.08);
for (const price of prices) {
totals.push(withTax(price));
}✅ Hoist the invariant
const TAX_RATE = 1.08;
for (const price of prices) {
totals.push(price * TAX_RATE); // Simple and clear
}Why: When the invariant is a simple primitive, hoisting it outside the loop is clearer than creating a curried function.
4.7 Defer Await
Move await operations into the branches where they're actually used to avoid blocking code paths that don't need them.
❌ Incorrect: blocks both branches
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId);
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true };
}
// Only this branch uses userData
return processUserData(userData);
}✅ Correct: only blocks when needed
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true };
}
// Fetch only when needed
const userData = await fetchUserData(userId);
return processUserData(userData);
}This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
4.3 Memoization and Redundant Calculations
Issues
- Loop-invariant code inside loops
- Repeated function calls with same arguments
- Constant expressions computed at runtime
- Expensive operations that could be memoized
- Trivial operations being memoized unnecessarily
Optimizations
- Hoist loop-invariant code
- Precompute constants at module load
- Memoize pure functions with limited input domain
- Cache results of expensive operations
- Avoid memoizing trivial computations
Examples
Avoid Trivial Memoization
❌ Incorrect: trivial computation
const ternMemo = memoize((pred) => pred ? 'Right!' : 'Wrong');✅ Correct: direct computation
const result = test ? 'Right!' : 'Wrong';Hoist Loop-Invariant Calculations
❌ Incorrect: calculations recomputed each iteration
for (let i = 0; i < items.length; i++) {
const prefix = config.namespace + '.';
const multiplier = Math.PI * 2;
process(items[i], prefix, multiplier);
}✅ Correct: hoist outside loop
const prefix = config.namespace + '.';
const multiplier = Math.PI * 2;
const len = items.length;
for (let i = 0; i < len; i++) {
process(items[i], prefix, multiplier);
}Precompute Constants
❌ Incorrect: compute at runtime
function calculateArea(radius) {
const pi = Math.PI;
return pi * radius * radius;
}✅ Correct: module-level constant
const PI = Math.PI;
function calculateArea(radius) {
return PI * radius * radius;
}Memoize Expensive Operations
❌ Incorrect: repeated expensive calls
function render() {
const data = parseAndTransformData(rawData);
return display(data);
}
// Called many times with same rawData
render();
render();
render();✅ Correct: memoize expensive function
const memoizedParse = memoize(parseAndTransformData);
function render() {
const data = memoizedParse(rawData);
return display(data);
}Cache Function Results
❌ Incorrect: repeated calculation
function processItems(items) {
for (const item of items) {
const config = getConfig(item.type);
apply(item, config);
}
}
function getConfig(type) {
// Expensive lookup/calculation
return expensiveOperation(type);
}✅ Correct: cache results
function processItems(items) {
const configCache = new Map();
for (const item of items) {
if (!configCache.has(item.type)) {
configCache.set(item.type, getConfig(item.type));
}
const config = configCache.get(item.type);
apply(item, config);
}
}Repeated Function Calls with Same Arguments
❌ Incorrect: call multiple times
function validate(data) {
if (isValid(data.user) && isComplete(data.user)) {
return processUser(data.user);
}
return null;
}
function isComplete(user) {
return isValid(user) && user.profile && user.settings;
}✅ Correct: call once, reuse result
function validate(data) {
const valid = isValid(data.user);
if (valid && isComplete(data.user, valid)) {
return processUser(data.user);
}
return null;
}
function isComplete(user, alreadyValid = false) {
return (alreadyValid || isValid(user)) && user.profile && user.settings;
}
## Fallback Patterns
When memoization doesn't work or creates problems, use these alternatives:
### Fallback 1: Skip memoization for trivial computations
**Scenario**: Computation is so cheap that caching overhead exceeds benefit// ❌ Memoization overhead > computation cost const cache = new Map<number, number>();
function double(n: number): number { if (cache.has(n)) return cache.get(n)!; const result = n * 2; cache.set(n, result); return result; }
**✅ Just compute directly**function double(n: number): number { return n * 2; }
**When to skip memoization**:
- Simple arithmetic (addition, multiplication, modulo)
- Property access on in-memory objects
- String concatenation of short strings
- Array index lookups
**Why**: Map overhead (hashing, storage, lookup) exceeds cost of trivial operations. Memoize only when computation cost significantly exceeds cache overhead.
### Fallback 2: Limit cache size to prevent memory leaks
**Scenario**: Cache grows unbounded and causes memory exhaustion// ❌ Unbounded cache can leak memory const cache = new Map<string, Result>();
function compute(input: string): Result { if (cache.has(input)) return cache.get(input)!; const result = expensiveOperation(input); cache.set(input, result); // Cache grows forever return result; }
**✅ Use LRU cache with size limit**class LRUCache<K, V> { private cache = new Map<K, V>(); private order: K[] = [];
constructor(private maxSize: number = 100) {}
get(key: K): V | undefined { const value = this.cache.get(key); if (value !== undefined) { // Move to end (most recently used) this.order = this.order.filter(k => k !== key); this.order.push(key); } return value; }
set(key: K, value: V): void { if (this.cache.has(key)) { this.order = this.order.filter(k => k !== key); } else if (this.cache.size >= this.maxSize) { // Evict least recently used const oldest = this.order.shift()!; this.cache.delete(oldest); }
this.cache.set(key, value); this.order.push(key); } }
const cache = new LRUCache<string, Result>(100);
function compute(input: string): Result { const cached = cache.get(input); if (cached) return cached;
const result = expensiveOperation(input); cache.set(input, result); return result; }
**Why**: Unbounded caches leak memory. LRU (Least Recently Used) eviction maintains fixed memory usage while keeping hot data cached.
### Fallback 3: Time-based cache invalidation for stale data
**Scenario**: Cached data becomes stale and must be refreshed// ❌ Cache never expires, returns stale data const cache = new Map<string, UserData>();
function getUserData(userId: string): UserData { if (cache.has(userId)) return cache.get(userId)!; const data = fetchUserData(userId); cache.set(userId, data); return data; }
**✅ Add TTL (time-to-live) to cache entries**interface CacheEntry<T> { value: T; expires: number; }
const TTL_MS = 5 60 1000; // 5 minutes const cache = new Map<string, CacheEntry<UserData>>();
function getUserData(userId: string): UserData { const entry = cache.get(userId); const now = Date.now();
if (entry && entry.expires > now) { return entry.value; // Still valid }
const data = fetchUserData(userId); cache.set(userId, { value: data, expires: now + TTL_MS, });
return data; }
**Why**: Some data changes over time (user profiles, prices, inventory). TTL ensures cache freshness without manual invalidation.
### Fallback 4: Recompute instead of complex cache key generation
**Scenario**: Cache key generation is expensive or complex// ❌ Complex cache key generation const cache = new Map<string, Result>();
function process(obj: ComplexObject): Result { // Expensive: serialize entire object to create key const key = JSON.stringify(obj); if (cache.has(key)) return cache.get(key)!;
const result = expensiveOperation(obj); cache.set(key, result); return result; }
**✅ Skip memoization, just recompute**function process(obj: ComplexObject): Result { return expensiveOperation(obj); }
**When to skip memoization due to key complexity**:
- Cache key requires serialization (JSON.stringify, hash computation)
- Objects are large or deeply nested
- Key generation cost approaches computation cost
**Why**: If key generation (serialization, hashing) costs 80% of the computation, memoization provides minimal benefit. Only memoize when key generation is trivial compared to computation.
### Fallback 5: Use WeakMap for object-keyed caches
**Scenario**: Caching results keyed by objects, need automatic cleanup// ❌ Regular Map prevents garbage collection const cache = new Map<object, Result>();
function process(obj: SomeObject): Result { if (cache.has(obj)) return cache.get(obj)!; const result = expensiveOperation(obj); cache.set(obj, result); // obj can never be GC'd return result; }
**✅ Use WeakMap for automatic cleanup**const cache = new WeakMap<object, Result>();
function process(obj: SomeObject): Result { const cached = cache.get(obj); if (cached) return cached;
const result = expensiveOperation(obj); cache.set(obj, result); return result; }
// When obj goes out of scope, cache entry is automatically removed
**Why**: WeakMap allows garbage collection of keys. When the object is no longer referenced elsewhere, the cache entry is automatically cleaned up, preventing memory leaks.
### Fallback 6: Precomputation instead of runtime memoization
**Scenario**: All possible inputs are known ahead of time// ❌ Compute and cache at runtime const cache = new Map<number, number>();
function factorial(n: number): number { if (cache.has(n)) return cache.get(n)!; const result = n <= 1 ? 1 : n * factorial(n - 1); cache.set(n, result); return result; }
**✅ Precompute all values at startup**// Precompute factorials for n = 0 to 20 const FACTORIALS = [ 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800, 479001600, 6227020800, 87178291200, 1307674368000, 20922789888000, 355687428096000, 6402373705728000, 121645100408832000, 2432902008176640000 ];
function factorial(n: number): number { if (n < 0 || n >= FACTORIALS.length) { throw new Error(Factorial(${n}) out of range [0, ${FACTORIALS.length - 1}]); } return FACTORIALS[n]; }
**When to precompute**:
- Input space is small and finite (< 1000 values)
- All inputs are known at compile time
- Computation is expensive but only needs to run once
**Why**: Precomputation eliminates runtime overhead (cache checks, storage). Array lookup is faster than Map lookup. Memory cost is paid upfront and constant.4.10 Object Operations
Issues
- Object spreading in loops
- Deep cloning when shallow clone suffices
- Unnecessary object creation
- Object.keys/values/entries on hot paths
- Spread operators for single property changes
Optimizations
- Mutate when safe (function owns object, local scope, not returned/exposed)
- Use Object.assign for shallow updates when immutability required
- Preallocate objects with known shape
- Direct property assignment over spreading when object is owned
Examples
Mutate When Safe
❌ Incorrect: unnecessary spread in loop
let result = {};
for (const item of items) {
result = { ...result, [item.id]: item.value };
}✅ Correct: mutate owned object
const result = {};
for (const item of items) {
result[item.id] = item.value;
}Shallow vs Deep Clone
❌ Incorrect: deep clone for simple update
import { cloneDeep } from 'lodash';
function updateUser(user, name) {
const updated = cloneDeep(user);
updated.name = name;
return updated;
}✅ Correct: shallow clone with Object.assign
function updateUser(user, name) {
return Object.assign({}, user, { name });
}Single Property Update
❌ Incorrect: spread for one property
function setActive(state, active) {
return {
...state,
active,
};
}✅ Correct: Object.assign for performance
function setActive(state, active) {
return Object.assign({}, state, { active });
}Preallocate Object Shape
❌ Incorrect: dynamic property addition
function buildConfig(data) {
const config = {};
if (data.x) config.x = data.x;
if (data.y) config.y = data.y;
if (data.z) config.z = data.z;
return config;
}✅ Correct: preallocate shape
function buildConfig(data) {
return {
x: data.x || 0,
y: data.y || 0,
z: data.z || 0,
};
}Object.keys on Hot Paths
❌ Incorrect: repeated Object.keys
for (const item of items) {
const keys = Object.keys(item);
if (keys.length > 0) {
process(item);
}
}✅ Correct: use for...in or check properties directly
for (const item of items) {
if (item.id !== undefined) {
process(item);
}
}Unnecessary Object Creation
❌ Incorrect: create object just to destructure
function getCoords(x, y) {
const point = { x, y };
return processPoint(point);
}
function processPoint({ x, y }) {
return Math.sqrt(x * x + y * y);
}✅ Correct: pass values directly
function getCoords(x, y) {
return processPoint(x, y);
}
function processPoint(x, y) {
return Math.sqrt(x * x + y * y);
}4.11 Additional Performance Concerns
Issues
- String concatenation in loops (use array join)
- Regular expression creation in loops
- Synchronous I/O on hot paths
- Unnecessary async/await overhead
- try/catch in tight loops (deoptimization risk)
- Closures capturing large scopes
- Function bind/arrow functions in render paths
Optimizations
- Batch string operations
- Compile regex once, reuse
- Use async I/O with proper batching
- Remove unnecessary async wrappers
- Move error handling outside hot loops
- Minimize closure scope
- Prebind functions outside loops/renders
Examples
String Building with Array Join
String concatenation creates new strings on each operation. Use array join for building strings in loops.
❌ Incorrect: repeated string concatenation
let result = '';
for (const item of items) {
result += item.name + ', ';
}✅ Correct: array join
const parts = [];
for (const item of items) {
parts.push(item.name);
}
const result = parts.join(', ');Regular Expression in Loops
❌ Incorrect: create regex each iteration
for (const text of texts) {
const matches = text.match(/\d+/g);
process(matches);
}✅ Correct: compile once
const digitRegex = /\d+/g;
for (const text of texts) {
const matches = text.match(digitRegex);
process(matches);
}Unnecessary Async Overhead
❌ Incorrect: async wrapper with no await
async function getUser(id) {
return users.find(u => u.id === id);
}✅ Correct: synchronous function
function getUser(id) {
return users.find(u => u.id === id);
}try/catch in Tight Loops
❌ Incorrect: error handling in loop
for (let i = 0; i < items.length; i++) {
try {
process(items[i]);
} catch (err) {
logError(err);
}
}✅ Correct: move error handling outside
try {
for (let i = 0; i < items.length; i++) {
process(items[i]);
}
} catch (err) {
logError(err);
}
// Or handle errors in the called functionClosures Capturing Large Scopes
❌ Incorrect: capture entire context
function createProcessor(largeConfig) {
return function process(item) {
return item.value * largeConfig.data.nested.multiplier;
};
}✅ Correct: minimize closure scope
function createProcessor(largeConfig) {
const multiplier = largeConfig.data.nested.multiplier;
return function process(item) {
return item.value * multiplier;
};
}Function Bind in Render Paths
❌ Incorrect: create new function each render
function Component({ items }) {
return items.map(item => (
<button onClick={() => handleClick(item.id)}>
{item.name}
</button>
));
}✅ Correct: prebind or use data attributes
function Component({ items }) {
const handleItemClick = (e) => {
handleClick(e.target.dataset.id);
};
return items.map(item => (
<button onClick={handleItemClick} data-id={item.id}>
{item.name}
</button>
));
}Async I/O on Hot Paths
❌ Incorrect: synchronous blocking
import { readFileSync } from 'fs';
function loadTemplate(name) {
return readFileSync(`./templates/${name}.html`, 'utf-8');
}
// Called in request handler
app.get('/page', (req, res) => {
const template = loadTemplate('home');
res.send(template);
});✅ Correct: async with caching
import { readFile } from 'fs/promises';
const templateCache = new Map();
async function loadTemplate(name) {
if (!templateCache.has(name)) {
const content = await readFile(`./templates/${name}.html`, 'utf-8');
templateCache.set(name, content);
}
return templateCache.get(name);
}
app.get('/page', async (req, res) => {
const template = await loadTemplate('home');
res.send(template);
});Template Literals vs Array Join
For complex string building with many parts, array join can be more efficient than template literals.
❌ Incorrect: multiple concatenations per iteration
function buildHTML(items) {
let html = '<ul>';
for (const item of items) {
html += '<li>' + item.name + '</li>';
}
html += '</ul>';
return html;
}✅ Correct: array join (better for many items)
function buildHTML(items) {
const parts = ['<ul>'];
for (const item of items) {
parts.push('<li>', item.name, '</li>');
}
parts.push('</ul>');
return parts.join('');
}Note: For small iterations (<100 items) or simple concatenations, template literals are fine and more readable.
4.5 Predictable Execution and Cache Locality
Issues
- Poor data structure layout for access pattern
- Random access patterns that could be sequential
- Struct-of-arrays vs array-of-structs mismatches
- Scattered memory access in tight loops
- Unpredictable control flow
Optimizations
- Sequential memory access patterns
- Group related data together
- Use flat arrays instead of nested structures
- Consider columnar layout for analytics workloads
- Write code with clear, predictable execution paths
Examples
Sequential vs Random Access
❌ Incorrect: random access pattern
const users = new Map();
users.set('id1', { name: 'Alice', age: 30 });
users.set('id2', { name: 'Bob', age: 25 });
// Random access through Map
for (const id of shuffledIds) {
process(users.get(id));
}✅ Correct: sequential access
const users = [
{ id: 'id1', name: 'Alice', age: 30 },
{ id: 'id2', name: 'Bob', age: 25 },
];
// Sequential memory access
for (let i = 0; i < users.length; i++) {
process(users[i]);
}Struct-of-Arrays vs Array-of-Structs
❌ Incorrect: array-of-structs for columnar access
const particles = [
{ x: 1, y: 2, vx: 0.1, vy: 0.2 },
{ x: 3, y: 4, vx: 0.3, vy: 0.4 },
// ... thousands more
];
// Need only x coordinates - poor cache usage
for (let i = 0; i < particles.length; i++) {
updateX(particles[i].x);
}✅ Correct: struct-of-arrays for columnar workload
const particles = {
x: [1, 3, ...],
y: [2, 4, ...],
vx: [0.1, 0.3, ...],
vy: [0.2, 0.4, ...],
};
// Sequential access to contiguous memory
for (let i = 0; i < particles.x.length; i++) {
updateX(particles.x[i]);
}Flat Arrays vs Nested Structures
❌ Incorrect: nested object access
const grid = {
rows: [
{ cells: [{ value: 1 }, { value: 2 }] },
{ cells: [{ value: 3 }, { value: 4 }] },
]
};
for (const row of grid.rows) {
for (const cell of row.cells) {
process(cell.value);
}
}✅ Correct: flat array
const values = [1, 2, 3, 4];
const width = 2;
for (let i = 0; i < values.length; i++) {
process(values[i]);
}Group Related Data
❌ Incorrect: scattered data access
const ids = [1, 2, 3];
const names = ['Alice', 'Bob', 'Charlie'];
const ages = [30, 25, 35];
for (let i = 0; i < ids.length; i++) {
processUser(ids[i], names[i], ages[i]);
}✅ Correct: grouped data
const users = [
{ id: 1, name: 'Alice', age: 30 },
{ id: 2, name: 'Bob', age: 25 },
{ id: 3, name: 'Charlie', age: 35 },
];
for (let i = 0; i < users.length; i++) {
const user = users[i];
processUser(user.id, user.name, user.age);
}Predictable Control Flow
❌ Incorrect: unpredictable branches
function process(items) {
for (const item of items) {
if (Math.random() > 0.5) {
handleA(item);
} else {
handleB(item);
}
}
}✅ Correct: predictable branches
function process(items) {
for (const item of items) {
if (item.type === 'A') {
handleA(item);
} else {
handleB(item);
}
}
}Quick Optimization Reference
Quick lookup for common performance bottleneck patterns and optimization categories.
Bottleneck → Category Mapping
Nested Loops (O(n²) Complexity)
Symptoms:
- Multiple nested
forloops .filter()inside.map()or other array methods- Array methods called inside loops
- Linear search (
.includes(),.find()) in hot paths
Category: Algorithmic optimization
Reference files:
- reduce-looping.md
- reduce-branching.md
Expected speedup: 10-1000x for large datasets
---
Repeated Expensive Computations
Symptoms:
- Same calculation performed multiple times
- Function called with same arguments repeatedly
- Computed values recalculated in loops
- Invariant operations inside loops
Category: Caching & memoization
Reference files:
- memoization.md
- cache-property-access.md
Expected speedup: 2-100x depending on computation cost
---
Allocation-Heavy Code
Symptoms:
- Many small object creations in loops
- Excessive use of spread operator (
...) - Creating temporary arrays (
.slice(),[...arr]) - String concatenation in loops
- Frequent garbage collection (visible in profiler)
Category: Allocation reduction
Reference files:
- avoid-allocations.md
- object-operations.md
Expected speedup: 1.5-5x plus reduced GC pauses
---
Storage API in Loops
Symptoms:
localStorage.getItem()in loops or hot pathssessionStoragereads inside iterations- Cookie parsing on every function call
- Storage API calls not cached
Category: Caching
Reference files:
- cache-storage-api.md
Expected speedup: 5-20x for storage-heavy operations
---
Sequential Async Operations
Symptoms:
- Multiple
awaitstatements in sequence awaitbefore conditional branches- Promise chains that could be parallel
- Blocking on I/O that could be deferred
Category: I/O optimization
Reference files:
- defer-await.md
- batching.md
Expected speedup: 2-10x for I/O-bound operations
---
Poor Memory Locality
Symptoms:
- Random access patterns in arrays
- Non-sequential iteration
- Struct of Arrays could be Array of Structs
- Cache misses visible in profiler
Category: Memory locality
Reference files:
- predictable-execution.md
Expected speedup: 1.5-3x for large datasets
---
Unbounded Iterations
Symptoms:
- Loops without maximum iteration counts
- Queue processing without bounds
- Recursive functions without depth limits
- User input controls iteration count
Category: Bounded execution
Reference files:
- bounded-iteration.md
Expected speedup: Prevents pathological cases (DoS prevention)
---
Profiler Output → Category Lookup
| Profiler Shows | Issue Type | Category | Reference Files |
|---|---|---|---|
Array.prototype.filter high % | Chained array methods | Algorithmic | reduce-looping.md |
Map.get in tight loop | Should cache lookup | Caching | cache-property-access.md |
Object.assign / spread high % | Object allocation | Allocation | object-operations.md, avoid-allocations.md |
localStorage.getItem frequent | Storage not cached | Caching | cache-storage-api.md |
| Long async function | Sequential awaits | I/O | defer-await.md |
| GC taking >10% time | Allocation pressure | Allocation | avoid-allocations.md |
| Random access pattern | Poor locality | Memory locality | predictable-execution.md |
| Nested loop consuming >20% | O(n²) algorithm | Algorithmic | reduce-looping.md, reduce-branching.md |
---
Decision Matrix: When to Optimize
┌─────────────────────────────────────────┐
│ Is profiler showing >5% time in code? │
└─────────┬───────────────────────────────┘
│
├─ NO ──→ Don't optimize (not a bottleneck)
│
└─ YES ──→ What's the issue?
│
├─ O(n²) or worse ──→ Algorithmic fix (10-1000x)
│ reduce-looping.md, reduce-branching.md
│
├─ Repeated computation ──→ Caching (2-100x)
│ memoization.md, cache-property-access.md
│
├─ Many allocations ──→ Reduce GC (1.5-5x)
│ avoid-allocations.md, object-operations.md
│
├─ Sequential I/O ──→ Parallel/defer (2-50x)
│ defer-await.md, batching.md
│
└─ Loop overhead ──→ Micro-optimization (1.05-2x)
currying.md, performance-misc.md
Only if hot path!---
Anti-Pattern Detection
Before loading any references, scan for these common anti-patterns and identify categories:
// ❌ O(n²) - Category: Algorithmic
// References: reduce-looping.md, reduce-branching.md
for (const item of itemsA) {
const match = itemsB.find(x => x.id === item.id);
}
// ❌ Repeated expensive computation - Category: Caching
// References: memoization.md, cache-property-access.md
for (let i = 0; i < array.length; i++) {
const result = expensiveFunction(sameInput);
}
// ❌ Property access in loop - Category: Caching
// References: cache-property-access.md
for (let i = 0; i < items.length; i++) {
process(config.settings.nested.property);
}
// ❌ Storage API not cached - Category: Caching
// References: cache-storage-api.md
function getValue() {
return JSON.parse(localStorage.getItem('key') || '{}');
}
// ❌ Sequential awaits - Category: I/O
// References: defer-await.md, batching.md
async function process() {
const a = await fetchA();
const b = await fetchB(); // Could be parallel
return combine(a, b);
}
// ❌ Allocation in loop - Category: Allocation
// References: avoid-allocations.md, object-operations.md
const results = [];
for (const item of items) {
results.push({ ...item, newProp: value }); // Spread creates new object
}When you see these patterns: 1. Identify the category from the comments 2. Load the reference files listed for that category 3. Find optimization patterns in the reference files
4.1 Reduce Branching
Issues
- Excessive nested conditionals (>3 levels)
- Switch statements that could be lookup tables
- Repeated condition checks in same scope
- Polymorphic branching on hot paths
- Type guards that could be avoided
Optimizations
- Convert switch/if-chains to object/Map lookups
- Hoist invariant conditions
- Use early returns to reduce nesting
- Replace runtime type checks with compile-time guarantees
Examples
Switch to Lookup Table
❌ Incorrect: conditional checks
if (thing === 'ONE') {
/*...*/
}
if (thing === 'TWO') {
/*...*/
}
if (thing === 'THREE') {
/*...*/
}✅ Correct: lookup table
const lookup = {
ONE: {/*...*/},
TWO: {/*...*/},
THREE: {/*...*/},
}
const action = lookup[thing];Nested Conditionals to Early Returns
❌ Incorrect: excessive nesting
function process(data) {
if (data) {
if (data.isValid) {
if (data.user) {
if (data.user.hasPermission) {
return doWork(data);
}
}
}
}
return null;
}✅ Correct: early returns
function process(data) {
if (!data) return null;
if (!data.isValid) return null;
if (!data.user) return null;
if (!data.user.hasPermission) return null;
return doWork(data);
}Hoist Invariant Conditions
❌ Incorrect: repeated checks
for (const item of items) {
if (config.enableFeature && item.active) {
process(item);
}
}✅ Correct: hoist invariant
if (config.enableFeature) {
for (const item of items) {
if (item.active) {
process(item);
}
}
}Runtime Type Checks to Compile-Time
❌ Incorrect: runtime branching
function format(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase();
} else {
return value.toFixed(2);
}
}✅ Correct: separate functions
function formatString(value: string): string {
return value.toUpperCase();
}
function formatNumber(value: number): string {
return value.toFixed(2);
}4.2 Reduce Looping
Issues
- Multiple passes over same array (
.map().filter().reduce()) - Unnecessary array creation (spreading, slicing)
- Array methods in loops
- Linear searches that could use Sets/Maps
- Incorrect collection type for access pattern
Optimizations
- Combine multiple array operations into single pass
- Use index-based loops for performance-critical paths
- Replace O(n) lookups with O(1) using Set/Map
- Use typed arrays for numeric data
- Reuse arrays when function owns them (local scope, not returned/exposed)
Examples
Chained Methods to Single Reduce
❌ Incorrect: multiple passes over array
const result = arr.filter(predicate).map(mapper);
// Pass 1: filter creates intermediate array
// Pass 2: map creates final array✅ Correct: single pass
const result = arr.reduce((acc, curr) =>
predicate(curr) ? [...acc, mapper(curr)] : acc,
[]
);
// Single pass: test and transform in one iterationWhy this matters: Each array method creates a new array and iterates all elements. For large arrays, this means:
- Extra memory allocation for intermediate arrays
- Cache misses from jumping between arrays
- Double the loop overhead (iterator setup, bounds checks)
For 10,000 items, chaining .filter().map() means 20,000+ iterations plus temporary array allocation. Single reduce = 10,000 iterations, zero intermediate arrays.
Linear Search to O(1) Lookup
❌ Incorrect: O(n) - searches entire array every time
const keys = Object.keys(someObj);
if (keys.includes(id)) { /**/ }
// In a loop, this becomes O(n * m):
for (const id of userIds) { // n iterations
if (keys.includes(id)) { /**/ } // m lookups each
}✅ Correct: O(1) - hash lookup
const keys = new Set(Object.keys(someObj));
if (keys.has(id)) { /**/ }
// In a loop, this is O(n + m):
for (const id of userIds) { // n iterations
if (keys.has(id)) { /**/ } // O(1) lookup each
}Why this matters: Array.includes() scans the entire array linearly. For 100 items and 100 lookups, that's 10,000 comparisons. Set.has() uses hashing for O(1) lookups: 100 items and 100 lookups = 200 operations (100 to build Set, 100 to lookup). That's a 50x speedup.
Array Methods in Loops
❌ Incorrect: nested iterations
for (const user of users) {
const active = items.filter(item => item.userId === user.id);
process(active);
}✅ Correct: build lookup once
const itemsByUser = new Map();
for (const item of items) {
if (!itemsByUser.has(item.userId)) {
itemsByUser.set(item.userId, []);
}
itemsByUser.get(item.userId).push(item);
}
for (const user of users) {
const active = itemsByUser.get(user.id) || [];
process(active);
}Unnecessary Array Creation
❌ Incorrect: creates intermediate arrays
const result = [...arr].slice(0, 10).map(transform);✅ Correct: process directly
const result = [];
const len = Math.min(arr.length, 10);
for (let i = 0; i < len; i++) {
result.push(transform(arr[i]));
}Index-Based Loops for Hot Paths
❌ Incorrect: slower iteration
for (const item of largeArray) {
// performance-critical operation
processPixel(item);
}✅ Correct: index-based
const len = largeArray.length;
for (let i = 0; i < len; i++) {
processPixel(largeArray[i]);
}Typed Arrays for Numeric Data
❌ Incorrect: generic array stores boxed numbers
const pixels = new Array(width * height);
for (let i = 0; i < pixels.length; i++) {
pixels[i] = Math.random() * 255;
}
// Each number is a heap-allocated object
// Array can contain mixed types (slow property access)✅ Correct: typed array uses contiguous memory
const pixels = new Uint8Array(width * height);
for (let i = 0; i < pixels.length; i++) {
pixels[i] = Math.random() * 255;
}
// Fixed-type, contiguous memory buffer
// Direct memory access without boxingWhy this matters:
1. Memory efficiency: Generic arrays store numbers as heap-allocated objects (~16 bytes each). Typed arrays use raw bytes (1 byte for Uint8, 4 bytes for Float32). For 1920×1080 image: generic array = ~31MB, Uint8Array = 2MB.
2. Cache locality: Typed arrays are contiguous memory buffers. CPU can prefetch and cache efficiently. Generic arrays are pointer arrays - each access may cause cache miss.
3. Predictable performance: V8 can't optimize generic arrays if types change. Typed arrays are monomorphic by definition - V8 generates optimal machine code.
Use typed arrays for: pixel data, audio samples, 3D coordinates, binary protocols, large numeric datasets.
Fallback Patterns
When the primary optimization doesn't work, use these alternatives:
Fallback 1: Keep chained methods for readability in cold paths
Scenario: Code runs infrequently or with small datasets
// In configuration loading (runs once at startup)
const activeUsers = users
.filter(u => u.status === 'active')
.map(u => u.name)
.sort();When to keep chained methods:
- Non-performance-critical code (configuration, initialization)
- Small datasets (< 100 items)
- Readability is more valuable than microsecond gains
Why: Single-pass reduce is harder to read. In cold paths with small data, clarity trumps optimization.
Fallback 2: Use for loop when reduce becomes unreadable
Scenario: Complex logic makes reduce hard to understand
// ❌ Reduce is technically correct but hard to parse
const result = items.reduce((acc, item) => {
if (item.active && item.score > 50) {
const processed = processItem(item);
if (processed.valid) {
acc.valid.push(processed);
} else {
acc.invalid.push(item.id);
}
}
return acc;
}, { valid: [], invalid: [] });✅ Use explicit for loop
const result = { valid: [], invalid: [] };
for (const item of items) {
if (!item.active || item.score <= 50) continue;
const processed = processItem(item);
if (processed.valid) {
result.valid.push(processed);
} else {
result.invalid.push(item.id);
}
}Why: When reduce logic becomes nested or complex, explicit loops are clearer. Performance difference is minimal, readability difference is significant.
Fallback 3: Keep Set.has() but add graceful degradation
Scenario: Set might contain many items, memory becomes a concern
// ❌ Set could consume excessive memory for very large datasets
const keys = new Set(allKeys); // allKeys has 10M items
for (const id of userIds) {
if (keys.has(id)) { /* ... */ }
}✅ Add size check and fallback
const MAX_SET_SIZE = 100000;
let lookup: Set<string> | string[];
if (allKeys.length > MAX_SET_SIZE) {
// Fall back to sorted array with binary search
lookup = [...new Set(allKeys)].sort();
} else {
lookup = new Set(allKeys);
}
function contains(id: string): boolean {
if (lookup instanceof Set) {
return lookup.has(id);
}
// Binary search on sorted array
let left = 0;
let right = lookup.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (lookup[mid] === id) return true;
if (lookup[mid] < id) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}Why: Set.has() is O(1) but uses ~10x memory per element compared to arrays. For very large datasets, sorted array with binary search (O(log n)) may be better.
Fallback 4: Use Array.includes() with small arrays
Scenario: Array has few items and won't grow
// ✅ Array.includes() is fine here
const ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'DELETE'];
if (ALLOWED_METHODS.includes(method)) {
// Only 4 items - Set overhead not worth it
}When Array.includes() is acceptable:
- Array has < 10 items
- Array is constant (not built dynamically)
- Lookup happens infrequently
Why: Set overhead (construction, memory) exceeds benefit for small constant arrays. Linear search of 10 items is faster than Set construction.
Fallback 5: Typed arrays with fallback for compatibility
Scenario: Need typed array performance but must support old environments
function createBuffer(size: number): Uint8Array | number[] {
try {
return new Uint8Array(size);
} catch {
// Fall back to regular array in environments without TypedArray support
return new Array(size).fill(0);
}
}
function setValue(buffer: Uint8Array | number[], index: number, value: number): void {
buffer[index] = Math.floor(value) & 0xFF; // Works for both types
}Why: Typed arrays aren't supported everywhere. Graceful degradation maintains functionality while optimizing for modern environments.
Fallback 6: Index-based loop with early exit
Scenario: Need to stop iteration early, for...of doesn't allow break with value
// ❌ for...of doesn't work well with early exit + return value
function findFirstMatch(items: Item[]): Item | undefined {
for (const item of items) {
if (matches(item)) {
return item; // Works but not optimal for large arrays
}
}
}✅ Index-based loop for early exit
function findFirstMatch(items: Item[]): Item | undefined {
const len = items.length;
for (let i = 0; i < len; i++) {
if (matches(items[i])) {
return items[i]; // Can exit immediately
}
}
return undefined;
}Why: Built-in methods like .find() create function call overhead. Index-based loops are fastest for early-exit scenarios and allow fine-grained control.
Related skills
How it compares
Use accelint-ts-performance for V8-aware runtime audits rather than accelint-ts-best-practices when the goal is speed and allocation reduction, not type safety.
FAQ
What anti-patterns does accelint-ts-performance detect?
accelint-ts-performance finds O(n²) nested loops, excessive allocations, blocking async I/O, try/catch in hot paths, and V8 deoptimization issues. Each finding includes severity, expected gains, and remediation examples.
When should developers use accelint-ts-performance?
accelint-ts-performance fits measurably slow JavaScript or TypeScript code, hot-path profiling, and allocation-heavy utilities. The skill prioritizes algorithmic complexity fixes before micro-optimizations.