perf(hasher,init,tasks-runner): eliminate redundant lookups and loop allocations
Current Behavior
Several performance bottlenecks exist in hot paths:
- hash-task.ts:
getCustomHasher()is called twice per task with custom hashers - once to check existence, then again to use it - init-v2.ts: Plugin map object is spread inside a loop for every package.json file
- utils.ts:
Object.keys().forEach()chains create intermediate arrays
Expected Behavior
- Custom hasher lookup happens once and result is reused
- Plugin map created once before loop
- Direct
for...initeration without intermediate arrays
Summary of Changes
1. hash-task.ts - Cache Custom Hasher Lookup (10-20% faster)
┌─────────────────────────────────────────────────────────────────────────┐
│ BEFORE: getCustomHasher called twice per task │
│ │
│ for (task of tasks) { │
│ const hasher = getCustomHasher(task, graph); // ← CALL 1 │
│ if (hasher) tasksWithCustom.push(task); │
│ } │
│ tasksWithCustom.map(async (task) => { │
│ const hasher = getCustomHasher(task, graph); // ← CALL 2 (REDUNDANT) │
│ }); │
├─────────────────────────────────────────────────────────────────────────┤
│ AFTER: Store [task, hasher] tuple to reuse │
│ │
│ for (task of tasks) { │
│ const hasher = getCustomHasher(task, graph); // ← ONLY CALL │
│ if (hasher) tasksWithCustom.push([task, hasher]); │
│ } │
│ tasksWithCustom.map(async ([task, hasher]) => { │
│ await hasher(task, context); // ← Reuse cached │
│ }); │
└─────────────────────────────────────────────────────────────────────────┘
2. init-v2.ts - Move Plugin Map Outside Loop (15-30% faster)
┌─────────────────────────────────────────────────────────────────────────┐
│ BEFORE: Object spread on EVERY package.json │
│ │
│ for (file of packageJsonFiles) { │
│ const pluginMap = { ...npmPackageToPluginMap }; // ← SPREAD EVERY │
│ for ([dep, plugin] of Object.entries(pluginMap)) { ... } │
│ } │
├─────────────────────────────────────────────────────────────────────────┤
│ AFTER: Create once, reuse for all files │
│ │
│ const pluginMap = includeAngular │
│ ? { ...npmPackageToPluginMap, '@angular/cli': '@nx/angular' } │
│ : npmPackageToPluginMap; // ← SPREAD ONCE │
│ const entries = Object.entries(pluginMap); // ← CONVERT ONCE │
│ for (file of packageJsonFiles) { │
│ for ([dep, plugin] of entries) { ... } │
│ } │
└─────────────────────────────────────────────────────────────────────────┘
3. utils.ts - for...in Instead of Object.keys().forEach() (10-15% faster)
┌─────────────────────────────────────────────────────────────────────────┐
│ BEFORE: Creates intermediate arrays │
│ │
│ Object.keys(taskGraph.tasks).forEach((t) => { ... }); │
│ Object.keys(taskGraph.dependencies).forEach((taskId) => { │
│ taskGraph.dependencies[taskId].forEach((d) => { ... }); │
│ }); │
├─────────────────────────────────────────────────────────────────────────┤
│ AFTER: Direct iteration │
│ │
│ for (const t in taskGraph.tasks) { ... } │
│ for (const taskId in taskGraph.dependencies) { │
│ for (const d of taskGraph.dependencies[taskId]) { ... } │
│ } │
└─────────────────────────────────────────────────────────────────────────┘
Files Changed
packages/nx/src/hasher/hash-task.ts- Cache custom hasher lookuppackages/nx/src/command-line/init/init-v2.ts- Move plugin map outside looppackages/nx/src/tasks-runner/utils.ts- Use for...in loops
Related Issue(s)
Contributes to #33366
Merge Dependencies
This PR has no dependencies and can be merged independently.
Must be merged BEFORE: #33748
Deploy request for nx-docs pending review.
Visit the deploys page to approve it
| Name | Link |
|---|---|
| Latest commit | 117d6eefb7bba90aa9bd94d09ace17c1687aa4c4 |
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
The latest updates on your projects. Learn more about Vercel for GitHub.
| Project | Deployment | Preview | Updated (UTC) |
|---|---|---|---|
| nx-dev | Preview | Dec 8, 2025 5:54am |
@adwait1290 We are very grateful for your enthusiasm to contribute, I kindly request that you please stop sending these AI assisted micro-perf PRs now. In future, please open an issue regarding your plans and do not simply send pages worth of micro PRs without open communication.
Upon deeper inspection in some cases, we have found that they are not resulting in real-world performance wins, and instead create regressions because they are not considering memory and GC overhead of the whole system.
We will work on better benchmarking infrastructure on our side to have greater confidence in CI as to whether these kinds of PRs are actually net wins but for now each individual PR requires a thorough investigation by the team and you are sending far, far too many.
To reduce noise on the repo, I am going to close this, but rest assured it will be looked at as part of our performance optimization and benchmarking effort and merged in if it creates a provable net win.
Thank you once again for your keenness to help make Nx the best it can be, we really appreciate it!
This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request.