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

Mark transpiled JSX elements as pure #11126

Merged
merged 7 commits into from Mar 19, 2020

Conversation

nicolo-ribaudo
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo commented Feb 11, 2020

Q                       A
Fixed Issues? https://twitter.com/wSokra/status/1227163427879161857, closes #11105, fixes #11037
Patch: Bug Fix?
Major: Breaking Change?
Minor: New Feature? Yes
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes?
License MIT

React.createElement (and often also for other JSX implementation, but it's not guaranteed) calls are pure: they are meant to produce an object which then is used by the React (or other lib) runtime to actually instantiate the component.

We can mark them as such to help minifiers strip unused JSX elements.
Adding it to every component might not seem ideal, but:

  1. It can be easily gzipped, since it is exactly the same characters every time
  2. It helps minifier remove "real" code, and if something can't be removed minifiers will remove the comment anyway.

If some props aren't pure, minifiers won't remove them even if there is the annotation:

import { Foo, Bar } from "components";

export default function Component() {
  <Foo className={getClass()} prop="xx" />;
  return <Bar className="xx"/>
}

// -> After Babel

import { Foo, Bar } from "components";

export default function Component() {
  /*#__PURE__*/ React.createElement(Foo, {
    className: getClass(),
    prop: "xx"
  });
  return /*#__PURE__*/ React.createElement(Bar, {
    className: "xx"
  });
}

// -> After Terser (and Prettier)

import { Foo, Bar } from "components";
export default function Component() {
  return getClass(), React.createElement(Bar, { className: "xx" });
}

This PR solves #11037 in a different way than #11105, because we mark JSX elements as pure no matter if they are hoisted or not.

The output from that issue would now be

/*#__PURE__*/ React.createElement("svg", null, /*#__PURE__*/ React.createElement("path", null))

Which can be tree-shaken.

@nicolo-ribaudo nicolo-ribaudo added PR: New Feature 🚀 A type of pull request used for our changelog categories area: jsx labels Feb 11, 2020
Copy link
Member

@sokra sokra left a comment

Choose a reason for hiding this comment

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

Looks good to me. Here a few minor things:

@@ -1,3 +1,4 @@
/*#__PURE__*/
React.createElement(Component, babelHelpers.extends({}, props, {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe babelHelpers.extends should get a pure annotation too?

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe also other helpers are pure, I'll check 👍

/*#__PURE__*/
React.createElement(Composite, null,
/*#__PURE__*/
React.createElement(Composite2, null));
Copy link
Member

Choose a reason for hiding this comment

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

The output code would be a little bit more readable and less verbose if the comment would not be in a separate line but in the same line:

var x =
/*#__PURE__*/
React.createElement(Composite, null,
/*#__PURE__*/
React.createElement(Composite2, null));

vs

var x = /*#__PURE__*/ React.createElement(Composite, null, /*#__PURE__*/ React.createElement(Composite2, null));

But this isn't important...

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree, I'll do it in a separate PR since it isn't strictly related to adding the annotation to JSX elements.

@devongovett
Copy link
Contributor

I think it’s worth being careful about this. One could make a custom JSX pragma that is not pure and this would break that. Maybe only do it for the default React.createElement? Or have a whitelist (e.g. preact, etc.)?

@JLHwung
Copy link
Contributor

JLHwung commented Feb 11, 2020

Yeah, I agree on @devongovett , I think we should limit pure annotation when options.pragma === "React.createElement" and options.pragmaFrag === "React.Fragment".

We can offer limited whitelist and finally a pragmaIsPure: boolean option for custom pragmas.

@nicolo-ribaudo
Copy link
Member Author

I don't think that we can have a whitelist, because the pragma isn't based on the imported module but on the variable name (for example, h for Preact).

@developit When this PR is merged, Preact can benefit from these changes if the pure: true option is added to https://preactjs.com/guide/v10/getting-started/#setting-up-jsx and https://preactjs.com/guide/v8/switching-to-preact/#via-babel.

@JLHwung I didn't add the check for pragmaFrag because fragments are transpiled as pragmaFn(PragmaFrag, { props }), so the function is still always the pragma.

Copy link
Contributor

@JLHwung JLHwung left a comment

Choose a reason for hiding this comment

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

cc @gaearon since you have commented at #11105 :)

@nicolo-ribaudo
Copy link
Member Author

I was wondering if it would make sense to only add annotations to elements in the top level scope, since they are the ones preventing tree-shaking.

@JLHwung
Copy link
Contributor

JLHwung commented Feb 13, 2020

@nicolo-ribaudo Rollup can also shakes non top level functions, example is here.

@nicolo-ribaudo
Copy link
Member Author

Yeah, I'm wondering how common it is to have unused elements inside functions.
env === "development" && <Foo /> would be removed even without the annotation.

@nicolo-ribaudo nicolo-ribaudo force-pushed the jsx-pure branch 2 times, most recently from 13e7237 to b03ef6e Compare March 17, 2020 15:57
pure,
throwIfNamespace = true,
useSpread,
runtime = "classic",
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a breaking change from the "new JSX" PR, because that PR accepts runtime: false as an alias for runtime: "classic" while this one doesn't.

@lunaruan Do you think that this change is ok?


If this PR ends up being merged in a different release, I'll have to revert this line.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this should be okay. No preference for whether runtime: false should alias to runtime: 'classic' as long as runtime: 'classic' is the default behavior

@nicolo-ribaudo
Copy link
Member Author

I have finished adapting this PR after #11154 (I copied the same logic to the new transform)

@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 Jun 20, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 20, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: jsx outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: New Feature 🚀 A type of pull request used for our changelog categories
Projects
None yet
Development

Successfully merging this pull request may close these issues.

@babel/plugin-transform-react-constant-elements generates non-tree-shakeable output
8 participants