Visitar URL original
`compress.inline` changes memory semantics for closures · Issue #12344 · swc-project/swc · GitHub
Skip to content

compress.inline changes memory semantics for closures #12344

Open

Description

@lukebrody

Describe the bug

Problem

See this example

In the example above, running the original code (in Node 24.11.1):

undefined

running 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

https://play.swc.rs/?version=1.16.2&code=H4sIAAAAAAAAA11RQW7DIBC8%2BxV7C5YsJ7lVkdIn9FBF6tUELwkKhghw7MrK37tAqNz6glc7OzM7K0cjgrIGBn7DD%2B6cnZgfuNY1LBXQ5zCMzsAMx3dIDTbX1bOqcL5bF0CWeTsGdMwHHrCMCmt8gEn1CEdgdWKI%2FdaGK7oE2W5huuJaHZQHZbQy2Deggi8GHlyPCKNHD13S6jY%2BKgScQ6F6vGUA0YPnA5Y%2BSOuArIPQ1o%2BOED0KzR32JAUcvLB3bAqLt9Alnx1pS3RoBAHP39DFVbpsUOixz%2BNRrOhYudpk88%2F7Os512imSnHkMNqeWaopt%2F6pTZFQbnOAL%2Be0TJVtgor8DBEfsz7qqLtqeuT5dlW%2FjksfXUZbM1mSSQ37anjaTdJU46DGc1IAEZ%2FlO%2BYCZr70IVv8e1Gpstb2wPyRkvIH9blf%2FAOr5whxPAgAA&config=H4sIAAAAAAAAA32VPZLbMAyFe5%2FCo3qbFEmxB0i3Z%2BDQIijDK5IaAvRas%2BO7B%2FqznQRUZ%2BF7EMRHAP4%2BHI%2FNhdrm%2FfgtP%2BVhsJkgP54lQmNke5NIA22w1GYcuHnb6IUm5G1PMIfuC2nY5g54zqKfq7zpUyLY5GssYEQ%2FvhZsUxgyEL3EJIqxxzgl%2F3h7BqVICRCZ%2Fn7pynL60sAppR5s3EPGksHI0EHWZG3qezsQmKut8CA%2BIiW1yEQLgzNDToMuiA4ZU5S4gh1YZ9rkQGOYoWW8gpoo9SQxkhyyhh2cStfNHfAvh6vti2UtFW7zjcknK%2FCckNj4opqxwJoTC624jN5k4JK1mpeEsZb3CSA29JYo2qAaNSu8NFcd7qZi9NLVPCpcRkA9aYROrDWIXvN38gcyo1oygystTAa3O7hiBqEDA95L16j4C7k9q2V5HCB5jchNW%2FUYCzDVyVz5NCA7%2BLcclPVWWxXB8rlOaQynpE7Wlg58Tm5HINfBqY6z7I3bUOclOpAGAadKCqlgmls2nEw%2Fr9X%2FO0QmRd5puj6dXvbGqrg%2FFnawsXuO%2F7KzD6ugCcmVGa7%2FBtMdLzv8V%2FMUbRv78Q0N0seWybnA4f4HDHlm0lkGAAA%3D

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

Activity

  1. changed the title [-]`compress.inline` combines scopes, changing semantics compared to original code[/-] [+]`compress.inline` changes memory semantics for closures[/+] on Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions