openclaw - 💡(How to fix) Fix Performance regression in 2026.5.12 mirrors #70186 — jiti Babel traversal now dominates CLI startup (was: normalizeAliases)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Root Cause

This stack shows openclaw's already-bundled own dist files (protocol-*.js, client-*.js, call-*.js) being require()'d through jiti, which Babel-parses each one on every CLI invocation. The Node compile cache cannot help because jiti has its own translation pipeline.

Fix Action

Fix / Workaround

#StatusRelation
#70186closedSame class of regression (jiti dominates CLI startup), originally hot function normalizeAliases. Fix landed in 2026.4.21. In 2026.5.12 the hot function has shifted to Babel AST traversal (visitQueue/visit/traverseNode), so the original WeakMap patch no longer covers it. Practically a regression of the same user-visible symptom via a different code path.
#70533openIndependently confirmed here on 2026.5.12: a plugin.json continues to be openat'd ~405 times per invocation even when the plugin is disabled, so enable/disable is not a workaround. See "Disabling extensions does not help" below.
#59281openSame root tooling (jiti slow for TS source loading). Windows-specific; this report is Linux but the underlying jiti pattern looks similar.
#77347openPlugin loader LRU keyed by workspaceDir. Not applicable here (single workspace), but symptomatically related: cache layer exists but does not protect cold CLI calls.

Disable status does not gate manifest scanning. Each plugin.json is still opened ~405 times even when the plugin is disabled. This independently confirms #70533 on 2026.5.12, and rules out plugins disable as a workaround.

Code Example

time openclaw models           # ~37 s
time openclaw models           # ~37 s again (no warm-up benefit)
time openclaw gateway status   #  ~2.2 s
time openclaw plugins update --all   # ~5 s

---

$ time openclaw models > /dev/null
real    0m37.156s
user    0m44.970s
sys     0m4.797s

---

ajv compileSchema           node_modules/ajv/dist/compile/index.js:32
ajv _compileSchemaEnv       node_modules/ajv/dist/core.js:468
ajv compile                 node_modules/ajv/dist/core.js:157
(anon)                      dist/protocol-QC2mQFMN.js
eval_evalModule             node_modules/jiti/dist/jiti.cjs
jitiRequire                 node_modules/jiti/dist/jiti.cjs
(anon)                      dist/client-BGs41kAq.js
eval_evalModule             node_modules/jiti/dist/jiti.cjs
jitiRequire                 node_modules/jiti/dist/jiti.cjs
(anon)                      dist/call-DO7ujqcl.js

---

$ strace -f -e trace=openat openclaw models 2>/tmp/trace.txt
$ wc -l /tmp/trace.txt
89550   /tmp/trace.txt

---

407x  ~/.openclaw/extensions/security-guidance/.claude-plugin/plugin.json
407x  ~/.openclaw/extensions/context7/.claude-plugin/plugin.json
405x  ~/.openclaw/extensions/superpowers/.codex-plugin/plugin.json     <-- DISABLED plugin
352x  ~/.openclaw/plugins/installs.json
345x  ~/.openclaw/extensions/superpowers/package.json
228x  ~/.openclaw/npm/package.json
RAW_BUFFERClick to expand / collapse

Performance regression in 2026.5.12 mirrors #70186 — jiti Babel traversal now dominates CLI startup (was: normalizeAliases)

TL;DR

After upgrading to 2026.5.12, every openclaw <subcommand> invocation takes 30–40 s of wall time on a warm Node compile cache. CPU profile shows 48% self time in jiti/dist/babel.cjs (runtime TS→JS transpilation of openclaw's own bundled dist/*.js modules), 10% in V8 GC, and ~12% in lstat/stat/existsSync/open syscalls.

This looks like the same class of regression fixed in #70186 (closed, jiti normalizeAliases O(n²)), but the hot function has shifted: normalizeAliases is no longer dominant — the cost is now in Babel AST traversal (visitQueue, visit, traverseNode, traverseFast, getOrCreateCachedPaths). The earlier fix capped one specific code path; in 2026.5.12 jiti is being re-entered via a different one.

This also reproduces the symptom described in #70533 (open, "Plugin discovery loads all manifests at boot regardless of tools.allow"): each plugin.json is openat'd ~405 times per CLI invocation, even when the plugin is disabled — confirmed empirically below.

Environment

  • openclaw: 2026.5.12 (commit f066dd2)
  • Node.js: v22.22.0
  • Platform: Linux x86_64
  • Installed globally via npm i -g openclaw~/.npm-global/lib/node_modules/openclaw
  • NODE_COMPILE_CACHE is auto-enabled by openclaw.mjs (module.enableCompileCache()); cache directory holds 2 820 compiled files — confirmed populated, does not help
  • 70 plugins loaded, 24 disabled, 0 errors (openclaw plugins doctor clean)
  • 3 bundle extensions installed (context7, security-guidance, superpowers)

Reproduction

time openclaw models           # ~37 s
time openclaw models           # ~37 s again (no warm-up benefit)
time openclaw gateway status   #  ~2.2 s
time openclaw plugins update --all   # ~5 s

Wall-time vs CPU-time confirms it is CPU-bound and multithreaded:

$ time openclaw models > /dev/null
real    0m37.156s
user    0m44.970s
sys     0m4.797s

The long-running WebSocket gateway itself (127.0.0.1:18789) is fine. Only the short-lived CLI invocations are slow — every one re-pays the full bootstrap.

Relationship to existing issues

#StatusRelation
#70186closedSame class of regression (jiti dominates CLI startup), originally hot function normalizeAliases. Fix landed in 2026.4.21. In 2026.5.12 the hot function has shifted to Babel AST traversal (visitQueue/visit/traverseNode), so the original WeakMap patch no longer covers it. Practically a regression of the same user-visible symptom via a different code path.
#70533openIndependently confirmed here on 2026.5.12: a plugin.json continues to be openat'd ~405 times per invocation even when the plugin is disabled, so enable/disable is not a workaround. See "Disabling extensions does not help" below.
#59281openSame root tooling (jiti slow for TS source loading). Windows-specific; this report is Linux but the underlying jiti pattern looks similar.
#77347openPlugin loader LRU keyed by workspaceDir. Not applicable here (single workspace), but symptomatically related: cache layer exists but does not protect cold CLI calls.

Evidence

1. CPU profile (warm cache, 44.7 s wall, 140 242 samples)

Captured via NODE_OPTIONS="--cpu-prof --cpu-prof-dir=/tmp/oc-prof --cpu-prof-interval=200" openclaw models.

Top files by self time:

self %self (ms)File
48.20 %21 561.8node_modules/jiti/dist/babel.cjs
10.37 %4 637.4(garbage collector)
7.22 %3 231.3lstat (syscall)
2.74 %1 226.4node_modules/jiti/dist/jiti.cjs
1.87 %835.8stat (syscall)
1.70 %761.8existsSync
1.24 %552.6open (syscall)
1.06 %472.0node:internal/modules/package_json_reader
1.03 %461.4dist/json-files-CahFuwKs.js
0.60 %267.4dist/installed-plugin-index-store-DetkjvO9.js
0.53 %235.1dist/discovery-BEbYTYvv.js
0.42 %189.9dist/manifest-registry-Dgt5v-vG.js
0.26 %117.7dist/plugin-cache-primitives-M9JN_JCw.js ← cache layer barely runs

Top functions by self time are all Babel AST traversal:

self %self (ms)FunctionFile
3.88 %1 733visitQueuejiti/babel.cjs
3.29 %1 472visitjiti/babel.cjs
2.95 %1 320getOrCreateCachedPathsjiti/babel.cjs
2.17 %971traverseNodejiti/babel.cjs
2.01 %901visitMultiplejiti/babel.cjs
1.96 %876traverseFastjiti/babel.cjs
1.10 %493visitjiti/babel.cjs
0.89 %399pushContextjiti/babel.cjs
0.87 %388popContextjiti/babel.cjs

Note: normalizeAliases (the function fixed in #70186) is no longer at the top — the fix held. But Babel AST traversal now dominates, and is happening on every CLI invocation against openclaw's own bundled modules.

Top files by inclusive time (file appears anywhere in stack):

incl %File
81.09 %dist/list.status-command-Buoxqzi2.js (the models command itself)
67.01 %dist/provider-runtime-DXB7r8u2.js
62.23 %dist/plugin-module-loader-cache-MuKAXPrS.js
61.99 %dist/plugin-load-profile-BSCTMdA8.js
61.09 %node_modules/jiti/dist/jiti.cjs
50.98 %dist/loader-DkTFEskE.js
48.99 %node_modules/jiti/lib/jiti.cjs
48.93 %node_modules/jiti/dist/babel.cjs
48.81 %dist/providers.runtime-Bj4QRzbJ.js
48.47 %dist/plugin-cache-primitives-M9JN_JCw.js
44.09 %(plugin runtime) @openclaw/codex/dist/index.js

plugin-cache-primitives-*.js is on the stack for 48% of all sample ticks but only 0.26% self time — the cache layer is traversed but never short-circuits.

2. Hot stack: jiti is transpiling openclaw's own bundled dist/* modules

One representative sample (43.6 ms self time at leaf):

ajv compileSchema           node_modules/ajv/dist/compile/index.js:32
ajv _compileSchemaEnv       node_modules/ajv/dist/core.js:468
ajv compile                 node_modules/ajv/dist/core.js:157
(anon)                      dist/protocol-QC2mQFMN.js
eval_evalModule             node_modules/jiti/dist/jiti.cjs
jitiRequire                 node_modules/jiti/dist/jiti.cjs
(anon)                      dist/client-BGs41kAq.js
eval_evalModule             node_modules/jiti/dist/jiti.cjs
jitiRequire                 node_modules/jiti/dist/jiti.cjs
(anon)                      dist/call-DO7ujqcl.js

This stack shows openclaw's already-bundled own dist files (protocol-*.js, client-*.js, call-*.js) being require()'d through jiti, which Babel-parses each one on every CLI invocation. The Node compile cache cannot help because jiti has its own translation pipeline.

3. Strace — file access patterns

$ strace -f -e trace=openat openclaw models 2>/tmp/trace.txt
$ wc -l /tmp/trace.txt
89550   /tmp/trace.txt

89 550 openat calls per single CLI invocation. Hot files (count of openat per invocation):

407x  ~/.openclaw/extensions/security-guidance/.claude-plugin/plugin.json
407x  ~/.openclaw/extensions/context7/.claude-plugin/plugin.json
405x  ~/.openclaw/extensions/superpowers/.codex-plugin/plugin.json     <-- DISABLED plugin
352x  ~/.openclaw/plugins/installs.json
345x  ~/.openclaw/extensions/superpowers/package.json
228x  ~/.openclaw/npm/package.json

3 820 unique JS modules are loaded from node_modules; on average each JS file is openat'd ~23 times per invocation.

4. Disabling extensions does not help (confirms #70533)

All 3 bundle extensions enabledAll 3 disabledΔ
openclaw models34.2 s37.0 s / 36.7 s+2.5 s (slower)
openclaw gateway status2.2 s2.3 s+0.1 s
openat total89 55089 542−8 (noise)
plugin.json reads per extension407405−2

Disable status does not gate manifest scanning. Each plugin.json is still opened ~405 times even when the plugin is disabled. This independently confirms #70533 on 2026.5.12, and rules out plugins disable as a workaround.

5. What was tried and did not help

All measured in sequence on the same host:

  • openclaw plugins registry --refresh — improved gateway status 4.5 s → 2.2 s, but models unchanged
  • openclaw plugins update --all — no impact on models
  • openclaw doctor --fix --non-interactive --yes — no impact on subsequent CLI timing (does restart gateway)
  • Disabling all 3 bundle extensions — see table above
  • Explicit NODE_COMPILE_CACHE=... — launcher already enables it; respawns to use packaged cache dir; cache contains 2 820 files
  • Removing 50 MB session-transcript jsonl files (agents/general/sessions/*.jsonl)
  • Removing memory.bak-pre-memory-arch/, stale openclaw.json.{bak,bak.N,clobbered.*,pre-*,last-good}
  • Removing stale ~/.openclaw/gateway-restart-intent.json
  • Removing stale /tmp/openclaw-1000/openclaw-claude-skills-* (24 directories)
  • Gateway restart (manual and via doctor)

Hypothesized contributing causes

  1. jiti is being used to require() openclaw's own already-bundled dist/* modules. Those are emitted as plain JS in the bundle; runtime transpilation shouldn't be needed. Routing them through jiti+Babel re-parses ~3 800 files per CLI invocation. The previous #70186 fix capped normalizeAliases; the cost has moved into Babel traversal.
  2. The plugin-cache-primitives cache layer is reached but does not short-circuit (48% inclusive vs 0.26% self). Either invalidated on every cold process start, or only memoizes intra-process and is never persisted.
  3. Plugin-manifest scanning is repeated by independent subsystems (#70533 root cause): a single plugin.json is opened ~400 times per invocation, regardless of enabled status.

Suggested directions

  • Audit which require() sites in dist/* are routed through jiti. For openclaw's own bundled output, prefer native require/import so the Node compile cache covers them.
  • Persist plugin discovery + manifest scan results to a single mtime-keyed cache file under ~/.openclaw/, valid across CLI invocations, so commands like openclaw models / gateway status / doctor don't re-pay the full ~37 s.
  • Memoize the manifest registry per process so independent subsystems share one read of each plugin.json.
  • Apply enabled/disabled and tools.allow filters at discovery time, before manifest read+validate (per #70533).

Profile artifact

The full 162 MB V8 .cpuprofile, 14 MB strace, and step-by-step reproduction script are available — I can attach via gist or upload on request.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Performance regression in 2026.5.12 mirrors #70186 — jiti Babel traversal now dominates CLI startup (was: normalizeAliases)