From c5391cf771527b81ea83afdf5b5172d87d7d27d7 Mon Sep 17 00:00:00 2001 From: Daijiro Wachi Date: Sat, 13 Jun 2026 02:20:56 +0900 Subject: [PATCH] repl: lazy-load acorn and defer vm context creation Merely loading the repl builtin used to eagerly require acorn and acorn-walk (~250KB of JS) and create an entire V8 context via vm.runInNewContext() just to enumerate global property names, even though both are only needed once REPL input is actually parsed or tab-completion is used. Require acorn and acorn-walk at their function-level use sites instead, and wrap the global builtins set in getLazy(). The cost moves to the first preview/completion/recoverable-error check, where the one-time ~1ms is imperceptible. Benchmark results (Linux x64, misc/startup-core.js, 20 runs): require-builtins.js: +3.12% ops/s (t=3.57, p<0.01) import-builtins.mjs: +2.20% ops/s (t=3.00, p<0.01) In isolation, require('repl') drops from 4.64ms to 2.84ms (-39%) and interactive `node -i` startup improves by ~13%. Signed-off-by: Daijiro Wachi --- lib/internal/repl/completion.js | 18 ++++++++---------- lib/internal/repl/utils.js | 16 ++++++++++------ lib/repl.js | 10 ++++------ 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/internal/repl/completion.js b/lib/internal/repl/completion.js index 8587e5e5b2333b..e5b317bbe989cd 100644 --- a/lib/internal/repl/completion.js +++ b/lib/internal/repl/completion.js @@ -33,7 +33,7 @@ const { const { kContextId, getREPLResourceName, - globalBuiltins, + getGlobalBuiltins, getReplBuiltinLibs, fixReplRequire, } = require('internal/repl/utils'); @@ -61,13 +61,6 @@ const { getOwnNonIndexProperties, } = internalBinding('util'); -const { - isIdentifierStart, - isIdentifierChar, - parse: acornParse, -} = require('internal/deps/acorn/acorn/dist/acorn'); -const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk'); - const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/; const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/; const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/; @@ -87,6 +80,8 @@ function isIdentifier(str) { if (str === '') { return false; } + const { isIdentifierStart, isIdentifierChar } = + require('internal/deps/acorn/acorn/dist/acorn'); const first = StringPrototypeCodePointAt(str, 0); if (!isIdentifierStart(first)) { return false; @@ -374,8 +369,8 @@ function complete(line, callback) { if (!this.useGlobal) { // When the context is not `global`, builtins are not own // properties of it. - // `globalBuiltins` is a `SafeSet`, not an Array-like. - ArrayPrototypePush(contextOwnNames, ...globalBuiltins); + // `getGlobalBuiltins()` is a `SafeSet`, not an Array-like. + ArrayPrototypePush(contextOwnNames, ...getGlobalBuiltins()); } ArrayPrototypePush(completionGroups, contextOwnNames); if (filter !== '') addCommonWords(completionGroups); @@ -387,6 +382,7 @@ function complete(line, callback) { // so in order to make it correct we add an identifier to its end (e.g. `obj.foo.x`) const parsableCompleteTarget = completeTarget.endsWith('.') ? `${completeTarget}x` : completeTarget; + const { parse: acornParse } = require('internal/deps/acorn/acorn/dist/acorn'); let completeTargetAst; try { completeTargetAst = acornParse( @@ -551,6 +547,7 @@ function findExpressionCompleteTarget(code) { return !result ? result : `${result}.`; } + const { parse: acornParse } = require('internal/deps/acorn/acorn/dist/acorn'); let ast; try { ast = acornParse(code, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' }); @@ -625,6 +622,7 @@ function findExpressionCompleteTarget(code) { // Walk the AST for the current block of code, and check whether it contains any // statement or expression type that would potentially have side effects if evaluated. + const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk'); let isAllowed = true; const disallow = () => isAllowed = false; acornWalk.simple(lastBodyStatement, { diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index 1e6c71271755d9..653e14cbb0c071 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -20,10 +20,8 @@ const { Symbol, } = primordials; -const { tokTypes: tt, Parser: AcornParser } = - require('internal/deps/acorn/acorn/dist/acorn'); - const { sendInspectorCommand } = require('internal/util/inspector'); +const { getLazy } = require('internal/util'); const { ERR_INSPECTOR_NOT_AVAILABLE, @@ -80,6 +78,9 @@ function isRecoverableError(e, code) { isRecoverableError(e, `(${code}`)) return true; + const { tokTypes: tt, Parser: AcornParser } = + require('internal/deps/acorn/acorn/dist/acorn'); + let recoverable = false; // Determine if the point of any error raised is at the end of the input. @@ -756,6 +757,8 @@ function setupReverseSearch(repl) { const startsWithBraceRegExp = /^\s*{/; const endsWithSemicolonRegExp = /;\s*$/; function isValidSyntax(input) { + const { Parser: AcornParser } = + require('internal/deps/acorn/acorn/dist/acorn'); try { AcornParser.parse(input, { ecmaVersion: 'latest', @@ -815,8 +818,9 @@ function getREPLResourceName() { return `REPL${nextREPLResourceNumber++}`; } -const globalBuiltins = - new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)')); +// Creating a new context is expensive, so only do it on first use. +const getGlobalBuiltins = getLazy(() => + new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)'))); let _builtinLibs = ArrayPrototypeFilter( CJSModule.builtinModules, @@ -848,7 +852,7 @@ module.exports = { isValidSyntax, kContextId, getREPLResourceName, - globalBuiltins, + getGlobalBuiltins, getReplBuiltinLibs, setReplBuiltinLibs, fixReplRequire, diff --git a/lib/repl.js b/lib/repl.js index 17aab1c409beca..a00b7b3f372f38 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -89,10 +89,6 @@ const { makeRequireFunction, addBuiltinLibsToObject, } = require('internal/modules/helpers'); -const { - parse: acornParse, -} = require('internal/deps/acorn/acorn/dist/acorn'); -const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk'); const { decorateErrorStack, isError, @@ -153,7 +149,7 @@ const { isValidSyntax, kContextId, getREPLResourceName, - globalBuiltins, + getGlobalBuiltins, getReplBuiltinLibs, setReplBuiltinLibs, fixReplRequire, @@ -249,6 +245,8 @@ writer.options = { ...inspect.defaultOptions, showProxy: true }; // Converts static import statement to dynamic import statement const toDynamicImport = (codeLine) => { + const { parse: acornParse } = require('internal/deps/acorn/acorn/dist/acorn'); + const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk'); let dynamicImportStatement = ''; const ast = acornParse(codeLine, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' }); acornWalk.ancestor(ast, { @@ -1139,7 +1137,7 @@ class REPLServer extends Interface { }); ArrayPrototypeForEach(ObjectGetOwnPropertyNames(globalThis), (name) => { // Only set properties that do not already exist as a global builtin. - if (!globalBuiltins.has(name)) { + if (!getGlobalBuiltins().has(name)) { ObjectDefineProperty(context, name, { __proto__: null,