Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert private methods to fields when they are supported #12250

Conversation

nicolo-ribaudo
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo commented Oct 24, 2020

Q                       A
Fixed Issues? Fixes #12691
Patch: Bug Fix?
Major: Breaking Change?
Minor: New Feature? Yes
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes?
License MIT

This PR reduces the output size when targeting engines with native support for class fields, in two ways:

  1. It allows compiling decorators without compiling class fields in classes without decorators
  2. It transforms private methods to private fields when possible.

While (1) is just relaxing a check (but in the same package), (2) leverages the new api.targets() api, checking inside the private methods plugin if class fields are supported.

This means that, when using preset-env and using the top-level targets option, the effective compat data for proposal-class-properties change from

  "proposal-private-methods": {
    "chrome": "84",
    "opera": "70",
    "edge": "84",
    "electron": "10.0"
  },

to

  "proposal-class-properties": {
    "chrome": "74",
    "opera": "62",
    "edge": "79",
    "node": "12",
    "samsung": "11",
    "electron": "6.0"
  },

You can see here the output difference when targeting Node.js 12:

Input
export class A extends B {
  #foo = 2;
  #bar = 3;

  #incFoo() {
    this.#foo += super.getIncrement();
  }

  get #doubleFoo() {
    return this.#foo * 2;
  }

  run(twice) {
    this.#incFoo();

    return this.#doubleFoo;
  }
}
Output (before this PR)
import _get from "@babel/runtime/helpers/get";
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
import _classPrivateMethodGet from "@babel/runtime/helpers/classPrivateMethodGet";
import _classPrivateFieldSet from "@babel/runtime/helpers/classPrivateFieldSet";
import _classPrivateFieldGet from "@babel/runtime/helpers/classPrivateFieldGet";

var _foo = new WeakMap();

var _bar = new WeakMap();

var _incFoo = new WeakSet();

var _doubleFoo = new WeakMap();

export class A extends B {
  constructor(...args) {
    super(...args);

    _doubleFoo.set(this, {
      get: _get_doubleFoo,
      set: void 0
    });

    _incFoo.add(this);

    _foo.set(this, {
      writable: true,
      value: 2
    });

    _bar.set(this, {
      writable: true,
      value: 3
    });
  }

  run(twice) {
    _classPrivateMethodGet(this, _incFoo, _incFoo2).call(this);

    return _classPrivateFieldGet(this, _doubleFoo);
  }

}

var _incFoo2 = function _incFoo2() {
  _classPrivateFieldSet(this, _foo, _classPrivateFieldGet(this, _foo) + _get(_getPrototypeOf(A.prototype), "getIncrement", this).call(this));
};

var _get_doubleFoo = function () {
  return _classPrivateFieldGet(this, _foo) * 2;
};
Terser output (before this PR): 667 bytes
import e from"@babel/runtime/helpers/get";
import t from"@babel/runtime/helpers/getPrototypeOf";
import r from"@babel/runtime/helpers/classPrivateMethodGet";
import i from"@babel/runtime/helpers/classPrivateFieldSet";
import s from"@babel/runtime/helpers/classPrivateFieldGet";
var a=new WeakMap,l=new WeakMap,o=new WeakSet,n=new WeakMap;
export class A extends B{constructor(...e){super(...e),n.set(this,{get:p,set:void 0}),o.add(this),a.set(this,{writable:!0,value:2}),l.set(this,{writable:!0,value:3})}run(e){return r(this,o,h).call(this),s(this,n)}}
var h=function(){i(this,a,s(this,a)+e(t(A.prototype),"getIncrement",this).call(this))},p=function(){return 2*s(this,a)};
Output (with this PR)
import _get from "@babel/runtime/helpers/get";
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";

var _incFoo, _get_doubleFoo;

export class A extends B {
  #incFoo = _incFoo || (_incFoo = function () {
    this.#foo += _get(_getPrototypeOf(A.prototype), "getIncrement", this).call(this);
  });
  #doubleFoo = Object.defineProperty({
    t: this
  }, "_", {
    get: _get_doubleFoo || (_get_doubleFoo = function () {
      return this.t.#foo * 2;
    })
  });
  #foo = 2;
  #bar = 3;

  run(twice) {
    this.#incFoo();
    return this.#doubleFoo._;
  }

}
Terser output (with this PR): 370 bytes

(I had to replace # with $, because terser doesn't support private fields yet)

import t from"@babel/runtime/helpers/get";
import o from"@babel/runtime/helpers/getPrototypeOf";
var e,r;
export class A extends B{$incFoo=e||(e=function(){this.$foo+=t(o(A.prototype),"getIncrement",this).call(this)});$doubleFoo=Object.defineProperty({t:this},"_",{get:r||(r=function(){return 2*this.t.$foo})});$foo=2;$bar=3;run(t){return this.$incFoo(),this.$doubleFoo._}}

I don't how how to label this PR 😂 Maybe a new [smaller output]?

Comment on lines +1 to +13
class B {
#foo = 1;
bar = 2;
}

let A = babelHelpers.decorate(null, function (_initialize) {
"use strict";

class A {
constructor() {
_initialize(this);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example of feature (1): the first class is not transformed.

@babel-bot
Copy link
Collaborator

babel-bot commented Oct 24, 2020

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/33467/


export default declare((api, options) => {
api.assertVersion(7);

return createClassFeaturePlugin({
name: "proposal-private-methods",
if (!api.targets || isRequired("proposal-class-properties", api.targets())) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@developit was proposing to add something like api.targets.support(name) to the RFC, so that this line then becomes

  if (api.targets.support("proposal-class-properties")) {

@codesandbox-ci
Copy link

codesandbox-ci bot commented Oct 24, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 6a18821:

Sandbox Source
babel-repl-custom-plugin Configuration
babel-plugin-multi-config Configuration

Comment on lines 2 to 11
static #privateStaticFieldValue = Object.defineProperty({
t: this
}, "_", {
get: function () {
return Cl.#PRIVATE_STATIC_FIELD;
},
set: function (newValue) {
Cl.#PRIVATE_STATIC_FIELD = `Updated: ${newValue}`;
}
});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trick here is to rely on native getters/setters. By doing so we don't have to worry about replacing this.#foo, this.#foo += 2, etc. with complex function calls, since we can just replace this.#foo with this.#foo._.

used.fields = used.fields ?? elem;
}

if (elem.isPrivate() && elem.isProperty()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider merge this branch to

if (elem.isProperty()) {
      used.fields = used.fields ?? elem;
    }

"highlightCode": false,
"targets": { "node": 14 },
"plugins": [
["external-helpers", { "helperVersion": "7.1000.0" }],

This comment was marked as off-topic.

@nicolo-ribaudo nicolo-ribaudo added the PR: Output optimization 🔬 A type of pull request used for our changelog categories label Nov 3, 2020
@JLHwung
Copy link
Contributor

JLHwung commented Nov 4, 2020

Can you rebase since #12251 was reverted?

@nicolo-ribaudo
Copy link
Member Author

nicolo-ribaudo commented Nov 4, 2020

@JLHwung Wdyt about the last commit to reduce the output size for accessors? Using terser, the output size for the code in the original PR description is now 346 bytes:

import o from"@babel/runtime/helpers/get";
import t from"@babel/runtime/helpers/getPrototypeOf";
var e,r;
export class A extends B{$incFoo=e||(e=function(){this.$foo+=o(t(A.prototype),"getIncrement",this).call(this)});$doubleFoo={__proto__:r||(r={get _(){return 2*this.t.$foo}}),t:this};$foo=2;$bar=3;run(o){return this.$incFoo(),this.$doubleFoo._}}

t.objectProperty(
t.identifier("get"),
toMemoizedFunction(getter.node, path.scope, `get_${name}`),
t.classMethod(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be t.objectMethod?

@JLHwung
Copy link
Contributor

JLHwung commented Nov 27, 2020

CI error on Node.js 6 is related.

@nicolo-ribaudo
Copy link
Member Author

nicolo-ribaudo commented Feb 3, 2021

I was wondering if it would be better to implement it as a separate plugin, similarly to what we do for preset-env bugfixes.

Or at least if we this plugin should have a compileToFields: true | false | "auto" option (defaults to auto),

@nicolo-ribaudo nicolo-ribaudo deleted the branch babel:nicolo-ribaudo/targets-in-core June 9, 2021 23:07
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Sep 9, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 9, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue pkg: preset-env PR: Output optimization 🔬 A type of pull request used for our changelog categories Spec: Private Methods
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants