`compress.inline` changes memory semantics for closures
Describe the bug
Problem
See this example
In the example above, running the original code (in Node 24.11.1):
undefinedrunning the code after inlining:
{ weak: true }The inlining combines the scope of the narrowing function with that of its caller.
This is because v8 creates one context per scope (not one per closure).
Since the context retains everything in scope for all of its closures, if the caller also declares closures, the variables captured by those closures are now included in the callee's closure's context.
Impact
This makes the usage of narrowing factory functions to reduce closure retaining problematic when also using inlining.
Functions can be exempted from inlining using directives, but recognizing that inlining is a problem in these cases is an unintuitive stumbling block for programmers.
Because engine issues are unlikely to be solved in browsers (see referenced issues in this writeup: https://jakearchibald.com/2024/garbage-collection-and-closures/), programmers must be able to optimize closure memory usage.
Possible Solutions
- Don't inline a function whose body creates a closure into a scope that already creates one.
- Disable inlining by default. (In our project, the code size impact is negligible)
Input code
function makeNarrow(small) {
return x => small(x)
}
export function outer(state) {
const wide = () => state.other
// when makeNarrow is inlined, its return value uses `outer`'s context
// v8 uses the same context for all closures declared in a scope,
// so `state` referenced by `wide` is included in the context of makeNarrow's return value
return makeNarrow(state.small)
}
const small = 1
const other = new WeakRef({ weak: true })
globalThis.res = outer({ small, other: other.deref() })
setTimeout(() => {
global.gc()
console.log(other.deref())
}, 100)Config
{
"jsc": {
"parser": {
"syntax": "ecmascript",
"jsx": false
},
"target": "es5",
"loose": false,
"minify": {
"compress": {
"inline": 1,
"arguments": false,
"arrows": false,
"booleans": false,
"booleans_as_integers": false,
"collapse_vars": false,
"comparisons": false,
"computed_props": false,
"conditionals": false,
"dead_code": false,
"directives": false,
"drop_console": false,
"drop_debugger": false,
"evaluate": false,
"expression": false,
"hoist_funs": false,
"hoist_props": false,
"hoist_vars": false,
"if_return": false,
"join_vars": false,
"keep_classnames": false,
"keep_fargs": false,
"keep_fnames": false,
"keep_infinity": false,
"loops": false,
"negate_iife": false,
"properties": false,
"reduce_funcs": false,
"reduce_vars": false,
"side_effects": false,
"switches": false,
"typeofs": false,
"unsafe": false,
"unsafe_arrows": false,
"unsafe_comps": false,
"unsafe_Function": false,
"unsafe_math": false,
"unsafe_symbols": false,
"unsafe_methods": false,
"unsafe_proto": false,
"unsafe_regexp": false,
"unsafe_undefined": false,
"unused": false,
"const_to_let": false,
"pristine_globals": false
},
"mangle": false
}
},
"module": {
"type": "es6"
},
"minify": false,
"isModule": true
}Link to the code that reproduces this issue
SWC Info output
No response
Expected behavior
The code, with minification, outputs undefined
Actual behavior
The code, with minification, outputs { weak: true }
Version
1.16.2
Additional context
No response
Source: swc-project/swc