Registry
Aalexbit-codemod avatar

js-ts-perf-mining

Mine JavaScript/TypeScript performance issues via read-only JSSG metrics (N+1 I/O, async waterfalls, unbounded concurrency, expensive loop work, missing cleanup). Each step emits the same js_ts_perf_issue atom.

alexbit-codemod0 executions

  • mining
  • metrics
  • javascript
  • typescript
  • performance
  • n-plus-1
  • waterfall
  • concurrency
  • cleanup

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:

CardinalityValues
issueTypen_plus_1_io, async_waterfall, unbounded_concurrency, expensive_work_in_loop, missing_cleanup
confidencehigh, medium, low
filepathpath relative to the scan root
line1-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

StepissueTypeWhat it flags
n-plus-1-ion_plus_1_ioI/O inside for / for...of / while / .map / .forEach / .reduce, including item-dependent Promise.all(collection.map(...))
async-waterfallasync_waterfallSequential awaited I/O on one path where later calls do not consume earlier results
unbounded-concurrencyunbounded_concurrencyPromise.all / Promise.allSettled over .map / .flatMap of a non-literal collection
expensive-work-in-loopexpensive_work_in_loopLinear search, sort, parse/stringify, regex construction, DOM queries, clone/crypto/compression inside loops
missing-cleanupmissing_cleanupResource acquire without a matching release, especially in useEffect

Confidence

  • N+1 I/O — high when I/O arguments reference the iteration binding and the collection is not a small array literal; medium when 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. Bare Map.get / Set.add / obj.get are not I/O unless the receiver is a known client (db, queue, redis, …). Small static arrays, .slice, counted retry for loops (attempt <= maxAttempts, i < 3), and pagination/cursor/offset loops (for (let page = 1; ; page++), while (hasMore)) are not emitted. for...of / .map over a loaded collection still is.
  • Async waterfall — high for 3+ independent I/O awaits or two high-signal independent I/O calls; medium otherwise. 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/remove then insert/create) and same-stream write/close are treated as ordered and not chained. Nested awaits inside other functions (useCallback bodies) are not chained with sibling callbacks. Test/spec files, **/test/**, **/tests/**, **/__mocks__/**, /scripts/, and functions named main are skipped.
  • Unbounded concurrency — high when the collection is assigned from await / a DB-like call; medium for an identifier of unknown size. Array literals, .slice(0, n) (including aliases like checkFiles from .slice(0, 10)), bounded helpers (pLimit, pMap, chunk, batch, queue, Bottleneck), collections named batch / batch.files, and results / ownedFiles / ownedBatch aliases assigned from a batching call are not emitted.
  • Expensive work in loops — high for 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 a for...of iterable does not. Per-item JSON.parse(line), JSON via loop-local aliases (const out = item; JSON.stringify(out)), JSON.stringify used as a Map key, stringify inside throw, BFS .filter with no iteration binding, and /scripts/ paths are not emitted.
  • Missing cleanup — high for useEffect acquire with no matching cleanup; medium when a same-kind cleanup exists but identity does not match, or when an interval is outside useEffect. Timer identity includes ref.current = setInterval / ref.current = timeoutId / .push(setTimeout) paired with clearInterval(ref.current) or ref.current.forEach(clearTimeout). Returned cleanup identifiers resolve to a same-component helper (return clearExistingInterval). Process-lifetime subscribe and module-scope addEventListener are 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, boot main(), 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.

Ready to evolve your codebase?

Build tailored, compiler-aware skills to assess, plan, and automate complex codebase changes.