JavaScript / TypeScript¶
Beta
TypeScript and JavaScript analysis relies on name-based heuristics since these languages lack static type declarations. Expect higher false-positive rates than JVM languages, especially on large codebases. Use --confidence high to see only the most certain findings.
JavaScript arrays have a method called includes(). It does what you think — and it's O(n):
const processed = getProcessedIds();
for (const event of incoming) {
if (processed.includes(event.id)) {
skip(event);
}
}
Algorilla catches it:
warning · nested-lookup · Loop amplifiers · O(incoming × processed) → O(incoming + processed)
Linear includes on 'processed' inside for-each loop
→ Build a HashSet/Map from 'processed' before the loop
↗ https://tvinke.github.io/algorilla/rules/nested-lookup
3 │ for (const event of incoming) {
4 │ if (processed.includes(event.id)) {
⎿ for-each loop over incoming O(incoming)
⎿ includes on 'processed' O(processed) ← bottleneck
The fix is new Set(processed) and .has() instead of .includes(). Whether that matters depends on the size of processed — but now you can make that call consciously.
Top complexity traps in JavaScript/TypeScript¶
Array.includes() inside filter()¶
The most common finding in JS codebases. Array.includes() is O(n) — fine on its own, quadratic inside a loop or filter().
const allowedIds = getAllowedIds(); // returns Array
const filtered = items.filter(item =>
allowedIds.includes(item.id) // O(n) per item
);
warning · nested-lookup · O(items × allowedIds) → O(items + allowedIds)
→ Build a HashSet/Map from 'allowedIds' before the loop
↗ https://tvinke.github.io/algorilla/rules/nested-lookup
Fix: const allowedSet = new Set(allowedIds) then allowedSet.has(item.id). See nested-lookup.
await in a for loop¶
Sequential await inside a loop makes each iteration wait for the previous one. 100 API calls at 50ms each = 5 seconds. With Promise.all, it's ~50ms total.
for (const id of orderIds) {
const response = await fetch(`/api/orders/${id}`);
results.push(await response.json());
}
warning · io-in-loop · O(orderIds·IO) → O(1·IO + orderIds)
fetch() called inside for-each loop — IO per iteration
→ Batch the IO operation outside the loop, or use a bulk API
↗ https://tvinke.github.io/algorilla/rules/io-in-loop
Fix: Promise.all with orderIds.map(id => fetch(...)), or a batch endpoint. See io-in-loop.
new RegExp() on every iteration¶
Regex literals inside loops get recompiled per iteration. Not cheap — the regex engine builds an NFA/DFA on every call.
for (const line of lines) {
const parts = line.replace(/\s+/g, ' ');
const matched = line.match(/^(\d+)\s+(.*)$/);
}
warning · regex-recompilation-in-loop · O(|lines| × compile) → O(|lines|)
replace() compiles a regex on every call inside for-each loop
↗ https://tvinke.github.io/algorilla/rules/regex-recompilation-in-loop
Pull the regex out of the loop and assign it to a const. See regex-recompilation-in-loop.
React re-renders and expensive callbacks¶
Expensive operations inside callbacks passed to filter(), map(), or event handlers. In React, these run on every render if not memoized.
function OrderList({ orders, discounts }) {
const display = orders.map(order => {
const discount = discounts.find(d => d.productId === order.productId);
return <OrderRow order={order} discount={discount} />;
});
// ...
}
warning · nested-lookup · O(orders × discounts)
Linear find on 'discounts' inside map()
→ Build a HashSet/Map from 'discounts' before the loop
↗ https://tvinke.github.io/algorilla/rules/nested-lookup
Fix: build a Map from discounts before the map(), or wrap in useMemo(). See expensive-callback.
Regex recompilation in loops¶
This one surprises people. Regex literals inside loops get recompiled on every iteration:
for (const line of lines) {
const parts = line.replace(/\s+/g, ' ');
const matched = line.match(/^(\d+)\s+(.*)$/);
}
warning · regex-recompilation-in-loop · O(|lines| × compile) → O(|lines|)
replace() compiles a regex on every call inside for-each loop
↗ https://tvinke.github.io/algorilla/rules/regex-recompilation-in-loop
⎿ for-each loop O(|lines|)
⎿ line.replace() inside loop compile ← bottleneck
Pull the regex out of the loop, assign it to a const, done.
TypeScript type inference¶
When TypeScript type annotations are present, Algorilla uses them for more accurate analysis:
const ids: Set<string> = new Set(rawIds);
items.filter(i => ids.has(i.id)); // Not flagged — Set.has is O(1)
Without the type annotation, a new Set() is still recognized. But explicit types help in ambiguous cases.
Vue single-file components¶
The parser extracts <script> and <script setup> blocks from .vue files and analyzes the JavaScript/TypeScript within. Template expressions are not analyzed.
Supported extensions: .js, .mjs, .cjs, .ts, .tsx, .vue
Framework knowledge¶
| Framework | What Algorilla knows | Most relevant rules |
|---|---|---|
| Vue 3 | ref, reactive, computed, watch, lifecycle hooks, Pinia stores |
nested-lookup, expensive-callback |
| React | Hooks, components, render methods, React Router | nested-lookup, expensive-callback |
| Angular | DI, signals, change detection, lifecycle hooks | nested-lookup, io-in-loop |
| RxJS | Creation, transformation, filtering, join operators | sequential-async-join-in-loop |
| Lodash | Array, collection, object utilities | nested-lookup, repeated-linear-scan |
| Node.js / DOM | EventEmitter, Buffer, fs, Stream, Crypto | io-in-loop, expensive-construction |
For the full list, see Framework support.
Rules not available¶
Two rules target JVM-specific APIs and don't apply to JavaScript/TypeScript:
repeated-reflection-in-loop— Java reflection has no JS equivalentparallel-pipeline-bottleneck— parallel streams are a JVM concept
See also¶
- All rules — 26 of 28 rules apply to JavaScript/TypeScript
- Framework support — built-in knowledge of Vue, React, Angular, RxJS, Lodash, and Node.js
- Configuration — type hints, suppression, severity thresholds
- Understanding the output — severity levels, the complexity chain, confidence scores