Repository navigation
Inline destructured requires - #2087
Conversation
`inlineRequires` only inlined requires assigned to an identifier, so `const {foo} = require('foo')` stayed eager at module scope unless a Babel preset had lowered the destructuring first. `@react-native/babel-preset` stopped lowering destructuring for Hermes profiles in react/react-native#58965.
This inlines top-level object patterns with plain, renamed or string keys directly: each reference becomes `require('foo').foo`, matching the lowered form. With `memoizeCalls`, a module's properties share one memoized module. Defaults, rest elements, nesting and computed keys are left alone, as are reassigned bindings and properties assigned on the module.
Changelog: [Performance] Inline destructured CommonJS requires (`const {foo} = require('foo')`) under `inlineRequires`
| const name = path.scope.generateUid(moduleName); | ||
| declarationPath.remove(); | ||
| if ( | ||
| addStmtToBlock( |
There was a problem hiding this comment.
Some internal feedback:
Memoization silently dropped when addStmtToBlock fails after declaration already removed
Feedback: In the memoize branch, declarationPath.remove() is called unconditionally before addStmtToBlock succeeds. If addStmtToBlock returns false, memoVarName stays null but the original declaration is already gone, so references fall into the non-memoized path and emit an eager require('m').key per reference — silently defeating memoization.
There was a problem hiding this comment.
What’s the best thing to do given I’ve already merged it here? Assume follow up in a new PR?
| referencePath.replaceWith(getMember(getRequireExpr(), key)); | ||
| continue; | ||
| } | ||
| referencePath.scope.rename(memoVarName); |
There was a problem hiding this comment.
Swallowed replacement error leaves AST in partially-rewritten state without logging
Feedback: Each referencePath.replaceWith(...) is wrapped in a try/catch that only sets a boolean thrown flag and discards the error. If a later reference throws after earlier ones were rewritten, the loop continues mutating, and (in the memoize branch the declaration is already removed) the module is left with a mix of inlined and original references plus no logged cause.
Summary
Inline requires only inlines requires assigned to an identifier, ie
const foo = require('foo')orconst foo = require('foo').foo. Despite what the plugin's doc comment says,const {foo, bar} = require('foo')has never been inlined - it only appeared to work because Babel presets used to lower destructuring into the identifier form first.react/react-native#58965 stopped
@react-native/babel-presetlowering destructuring for Hermes profiles, so since then destructured CommonJS requires are evaluated eagerly at module init. ESM imports aren't affected.This adds direct support for top-level object patterns with plain, renamed or string keys. Each reference becomes
require('foo').bar, exactly what the lowered form produced. WithmemoizeCalls, a module's properties share one memoised module, so each scope requires it at most once:Defaults, rest elements, nested patterns and computed keys are left alone, as are reassigned bindings and properties assigned on the module elsewhere - the same constraints as the existing identifier forms.
Changelog: [Performance] Inline destructured CommonJS requires (
const {foo} = require('foo')) underinlineRequiresTest plan
New cases cover renamed and string keys, references across scopes and branches, unused requires and each unsupported pattern, with and without
memoizeCalls.End to end, I swapped this build into React Native's own Jest transform, which runs inline requires, on react/react-native#59303 (which preserves destructuring in that config). The 17 suites that relied on inline requires to load modules after their
jest.mockcalls pass in their original form, before react/react-native#59307 and react/react-native#59352 fixed them: 292 tests and 70 snapshots, against 110 failures without this change. RN's full suite passes too (6,289 tests).