perf(plugins): use native require for compiled JS before jiti

Every CLI invocation reads the config snapshot, which pulls bundled
channel doctor contracts and setup surfaces through
`getCachedPluginJitiLoader`. jiti's TS→JS transform pipeline adds
several seconds of per-load overhead on slower hosts (NAS profiling
shows ~78% of `openclaw config get` wall time spent inside the jiti
library), and that overhead is pure waste for the already-compiled
`.js` artifacts shipped in dist/.

Wrap the loader returned by `getCachedPluginJitiLoader` so that
compiled JS targets go through `tryNativeRequireJavaScriptModule`
first. Jiti stays on the hot path for:
- TS/TSX/MTS/CTS sources
- paths the native-require helper declines (Windows by default, or
  module-resolution fallbacks)

This centralises the fast path that already existed — inside
`doctor-contract-registry` and `channel-entry-contract` — and extends
it to every caller that goes through the jiti loader cache.

Benchmark on a modest NAS (Node 22.22, ZFS, telegram + discord
configured):

| command          | before | after |
|------------------|-------:|------:|
| config get X     |    24s |    6s |
| status           |    45s |   18s |
| devices list     |    55s |   26s |
| nodes status     |    55s |   26s |

Fixes the slow config/status/devices/nodes read paths reported in
openclaw#62842. Remaining time is dominated by non-jiti code paths
(config schema validation, eager provider-plugin module eval) that
are out of scope for this patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Effet
2026-04-24 21:59:19 +08:00
committed by Peter Steinberger
parent 6d60b035b4
commit b40b85c21a
4 changed files with 89 additions and 1 deletions

View File

@@ -972,6 +972,14 @@ describe("bundled channel entry shape guards", () => {
vi.doMock("../../plugins/channel-catalog-registry.js", () => ({
listChannelCatalogEntries: () => [],
}));
// jiti-loader-cache prefers native require() for compiled .js before
// falling back to jiti. This test drives plugin loading via the jiti
// mock — disable the native-require fast path so the mocked jiti loader
// is exercised instead of loading the on-disk fixture directly.
vi.doMock("../../plugins/native-module-require.js", () => ({
isJavaScriptModulePath: () => false,
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
}));
let reentered = false;
vi.doMock("jiti", () => ({

View File

@@ -202,4 +202,60 @@ describe("getCachedPluginJitiLoader", () => {
expect(firstAlias?.beta).toBe("/repo/alpha/sub");
expect((firstAlias as Record<symbol, unknown>)[marker]).toBe(true);
});
it("serves compiled .js targets from native require without invoking the jiti loader", async () => {
const jitiLoader = vi.fn();
const createJiti = vi.fn(() => jitiLoader);
vi.doMock("jiti", () => ({ createJiti }));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: (p: string) =>
p.endsWith(".js") || p.endsWith(".mjs") || p.endsWith(".cjs"),
tryNativeRequireJavaScriptModule: (target: string) => ({
ok: true,
moduleExport: { loadedFrom: target },
}),
}));
const { getCachedPluginJitiLoader } = await importFreshModule<
typeof import("./jiti-loader-cache.js")
>(import.meta.url, "./jiti-loader-cache.js?scope=native-require-fastpath");
const cache = new Map();
const loader = getCachedPluginJitiLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
});
const result = loader("/repo/dist/extensions/demo/api.js") as { loadedFrom: string };
expect(result.loadedFrom).toBe("/repo/dist/extensions/demo/api.js");
// jiti is created eagerly, but its loader must NOT be invoked for .js
// targets that `tryNativeRequireJavaScriptModule` resolves.
expect(jitiLoader).not.toHaveBeenCalled();
});
it("falls back to jiti when the native-require helper declines", async () => {
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
const createJiti = vi.fn(() => jitiLoader);
vi.doMock("jiti", () => ({ createJiti }));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
}));
const { getCachedPluginJitiLoader } = await importFreshModule<
typeof import("./jiti-loader-cache.js")
>(import.meta.url, "./jiti-loader-cache.js?scope=native-require-fallback");
const cache = new Map();
const loader = getCachedPluginJitiLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
});
const result = loader("/repo/dist/extensions/demo/api.js") as { fromJiti: boolean };
expect(result.fromJiti).toBe(true);
expect(jitiLoader).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
});
});

View File

@@ -1,4 +1,5 @@
import { createJiti } from "jiti";
import { tryNativeRequireJavaScriptModule } from "./native-module-require.js";
import {
buildPluginLoaderJitiOptions,
createPluginLoaderJitiCacheKey,
@@ -74,10 +75,24 @@ export function getCachedPluginJitiLoader(params: {
if (cached) {
return cached;
}
const loader = (params.createLoader ?? createJiti)(jitiFilename, {
const jitiLoader = (params.createLoader ?? createJiti)(jitiFilename, {
...buildPluginLoaderJitiOptions(aliasMap),
tryNative,
});
// The returned loader prefers native require() for already-compiled JS
// artifacts (the bundled plugin public surfaces shipped in dist/) because
// jiti's transform pipeline provides no value for output that is already
// plain JS and adds several seconds of per-load overhead on slower hosts.
// Jiti stays on the hot path for TS / TSX and for the small set of
// require(esm)/async-module fallbacks `tryNativeRequireJavaScriptModule`
// declines to handle.
const loader = ((target: string) => {
const native = tryNativeRequireJavaScriptModule(target);
if (native.ok) {
return native.moduleExport;
}
return jitiLoader(target);
}) as PluginJitiLoader;
params.cache.set(scopedCacheKey, loader);
return loader;
}

View File

@@ -8,6 +8,15 @@ import {
resetRegistryJitiMocks,
} from "./test-helpers/registry-jiti-mocks.js";
// jiti-loader-cache prefers native require() for compiled .js before falling
// back to jiti. These tests scripts plugin-loading behaviour through the
// jiti mock — disable the native-require fast path so the mocked jiti loader
// stays authoritative for the test fixture files on disk.
vi.mock("./native-module-require.js", () => ({
isJavaScriptModulePath: (_modulePath: string) => false,
tryNativeRequireJavaScriptModule: (_modulePath: string) => ({ ok: false }),
}));
const tempDirs: string[] = [];
const mocks = getRegistryJitiMocks();