js-ts-perf-mining
Read-only JSSG mining codemod that surfaces JavaScript/TypeScript performance issues. Each workflow step emits the same js_ts_perf_issue metric; no source files are modified.
Findings are candidates for review, not proof that code is slow or unsafe to change. Confirm with profiling and domain analysis before refactoring.
Metric
Every step exports and increments js_ts_perf_issue with the same cardinalities:
| Cardinality | Values |
|---|---|
issueType | n_plus_1_io, async_waterfall, unbounded_concurrency, expensive_work_in_loop, missing_cleanup |
confidence | high, medium, low |
filepath | path relative to the scan root |
line | 1-based line of the finding |
The same source site may emit more than once when it matches more than one detector (for example Promise.all(users.map(user => fetch(...))) is both N+1 I/O and unbounded concurrency).
Detectors
| Step | issueType | What it flags |
|---|---|---|
| n-plus-1-io | n_plus_1_io | I/O inside for / for...of / while / .map / .forEach / .reduce, including item-dependent Promise.all(collection.map(...)) |
| async-waterfall | async_waterfall | Sequential awaited I/O on one path where later calls do not consume earlier results |
| unbounded-concurrency | unbounded_concurrency | Promise.all / Promise.allSettled over .map / .flatMap of a non-literal collection |
| expensive-work-in-loop | expensive_work_in_loop | Linear search, sort, parse/stringify, regex construction, DOM queries, clone/crypto/compression inside loops |
| missing-cleanup | missing_cleanup | Resource acquire without a matching release, especially in useEffect |
Confidence
- N+1 I/O —
highwhen I/O arguments reference the iteration binding and the collection is not a small array literal;mediumwhen I/O is in a loop/map but the binding is not clearly in the arguments. Real I/O only:queue.getJob,db.update,fetch,findMany. BareMap.get/Set.add/obj.getare not I/O unless the receiver is a known client (db,queue,redis, …). Small static arrays,.slice, counted retryforloops (attempt <= maxAttempts,i < 3), and pagination/cursor/offset loops (for (let page = 1; ; page++),while (hasMore)) are not emitted.for...of/.mapover a loaded collection still is. - Async waterfall —
highfor 3+ independent I/O awaits or two high-signal independent I/O calls;mediumotherwise. A later await that uses an earlier binding as callee or receiver (await worker(uri),await api.internal…) is dependent. Consecutive mutate pairs (update/delete/removetheninsert/create) and same-streamwrite/closeare treated as ordered and not chained. Nested awaits inside other functions (useCallbackbodies) are not chained with sibling callbacks. Test/spec files,**/test/**,**/tests/**,**/__mocks__/**,/scripts/, and functions namedmainare skipped. - Unbounded concurrency —
highwhen the collection is assigned fromawait/ a DB-like call;mediumfor an identifier of unknown size. Array literals,.slice(0, n)(including aliases likecheckFilesfrom.slice(0, 10)), bounded helpers (pLimit,pMap,chunk,batch,queue,Bottleneck), collections namedbatch/batch.files, andresults/ownedFiles/ownedBatchaliases assigned from a batching call are not emitted. - Expensive work in loops —
highfor loop-invariant parse/regex/DOM/clone, or nested linear search whose callback references the enclosing iteration variable (join of A into B)..sort()in a loop body still emits;.sort()on afor...ofiterable does not. Per-itemJSON.parse(line), JSON via loop-local aliases (const out = item; JSON.stringify(out)),JSON.stringifyused as a Map key, stringify insidethrow, BFS.filterwith no iteration binding, and/scripts/paths are not emitted. - Missing cleanup —
highforuseEffectacquire with no matching cleanup;mediumwhen a same-kind cleanup exists but identity does not match, or when an interval is outsideuseEffect. Timer identity includesref.current = setInterval/ref.current = timeoutId/.push(setTimeout)paired withclearInterval(ref.current)orref.current.forEach(clearTimeout). Returned cleanup identifiers resolve to a same-component helper (return clearExistingInterval). Process-lifetimesubscribeand module-scopeaddEventListenerare not emitted./scripts/paths are skipped.
Cleanup pairing matches resource identity (same event + handler, same timer binding or ref path, same observer/socket/subscription, or the returned unsubscribe function), not merely the presence of any cleanup call.
Limits
This tool reports review-queue candidates on live request/job/component paths only. It does not:
- Prove N+1 queries, waterfalls, or leaks at runtime, or detect runtime
N - Model full TypeScript control-flow, effects ordering, or cross-function data dependencies (callee/receiver use of a prior await, same-stream write/close, pagination vs collection N+1, JSON aliases, helper/ref-assign cleanup, and slice aliases are heuristic)
- Scan tests,
__mocks__,/test/trees, bootmain(), or throwaway/scripts/(except N+1 / unbounded on cron-style scripts) - Auto-apply batching,
Promise.all,p-limit, Map lookups, or effect cleanups
All findings require human review before refactoring.