Uh oh!
There was an error while loading. Please reload this page.
Adds Babel plugin babel-plugin-optimize-react - #6219
Conversation
apostolos
commented
Jan 17, 2019
It doesn't pick up the correct reference to React when default import is not used (e.g. non-JSX modules). Let's say you have the following module: import{useRef,useEffect}from'react';exportfunctionusePrevious<T>(value: T){constref: React.MutableRefObject<T|null>=useRef<T>(null);useEffect(()=>{ref.current=value;});returnref.current;}Current output is: // EXTERNAL MODULE: ./node_modules/react/index.jsvarreact=__webpack_require__(602);varreact_default=/*#__PURE__*/__webpack_require__.n(react);// ...var_React=React,useRef=_React.useRef,useEffect=_React.useEffect;functionusePrevious(value){varref=useRef(null);useEffect(function(){ref.current=value;});returnref.current;}Expected output: // EXTERNAL MODULE: ./node_modules/react/index.jsvarreact=__webpack_require__(602);varreact_default=/*#__PURE__*/__webpack_require__.n(react);// ...varuseRef=react_default.useRef,useEffect=react_default.useEffect;functionusePrevious(value){varref=useRef(null);useEffect(function(){ref.current=value;});returnref.current;} |
Uh oh!
There was an error while loading. Please reload this page.
trueadm
commented
Jan 17, 2019
@apostolos I've published a new version to NPM – let me know if that fixes your issue. |
apostolos
commented
Jan 17, 2019
@trueadm 0.0.2 fixed the issue with the default import, thanks! I've found one more issue, although minor. The following is currently broken: importReactfrom'react';import{memo}from'react';I get the following error: ModuleParseError: Module parse failed: Identifier 'React' has already been declared (3:15)
You may need an appropriate loader to handle this file type.
| import React from 'react';| var __reactCreateElement__ = React.createElement;> import { memo, React } from 'react';The question is why bother since you can import both in one statement. It will probably cause issue with TypeScript users that don't use import*asReactfrom'react';import{useRef,useEffect,memo}from'react'; |
gaearon
commented
Jan 17, 2019
We definitely want to support all kinds of imports, thanks for reporting. |
gaearon
commented
Jan 17, 2019
(We should also check that |
@apostolos Thanks for reporting, I'll fix that. Update: fix published, you can find it on NPM now. |
| const babel = require('@babel/core'); | ||
| function transform(code) { | ||
| return babel.transform(code, { |
There was a problem hiding this comment.
this is gonna load babelrc / babel.config.js, not sure if you want that here
also - would be good to have tests both with & without @babel/plugin-transform-destructuring (and possibly some other overlapping combinations)
There was a problem hiding this comment.
I see you have no babelrc files in this package, but probably better disable config loading with
babel.transform(code,{babelrc: false,configFile: false,plugins: [plugin],}).codejust to be more future proof
There was a problem hiding this comment.
Good call on testing with other plugins. Destructuring optimization seems to be ignored when used alongside preset-env: optimize-react + preset-env.
I'll work on a PR to this one. At least with a test, hopefully with a fix.
| function createConstantCreateElementReference(reactReferencePath) { | ||
| const identifierName = reactReferencePath.node.name; | ||
| const binding = reactReferencePath.scope.getBinding(identifierName); | ||
| const createElementReference = t.identifier('__reactCreateElement__'); |
There was a problem hiding this comment.
this optimization is OK, but it works on per module (file) basis - it doesnt take into account that production bundles use scope hoisting, maybe we could somehow end up with single constant reference per chunk instead of many?
There was a problem hiding this comment.
That would be good, but I was unsure how to do that?
There was a problem hiding this comment.
Don't hardcode the identifier name, use path.scope.generateUidIdentifier in Program and save its value on state (the second argument to a visitor, which also is === this)
There was a problem hiding this comment.
I guess having a hardcoded name actually does let what Andarist suggested work, but not in a way that works with a minifier. You'd have to do var ref = ref || React.createElement over and over and you can't prove to a minifier the result of that expression is guaranteed truthy. Scope hoisting would rename the local vars anyway.
The only surefire way to get this working with scope hoisting is to have an ESM export of React. It is possible to generate one with a webpack plugin, and then use a Babel plugin to rewrite imports of React to import the generated ESM wrapper. (e.g. rewrite "from 'react'" to be "from 'react-esm-loader!react'")
This way, you could ensure that only other ESM would be importing the shared React ESM, and scope hoisting would do its job.
That'd stop working as well if there are dynamic imports, though, as webpack would be forced to put the modules shared with more than one chunk in a separate, non-scope-hoisted module. Perhaps another webpack plugin pass could detect this case and generate one wrapper module per chunk.
There was a problem hiding this comment.
We don't enable scope hoisting in CRA. Tbh I think it's reasonable compromise for now.
There was a problem hiding this comment.
We don't enable scope hoisting in CRA. Tbh I think it's reasonable compromise for now.
Are you sure? I haven't checked it, but it's enabled by default in webpack@4 in production mode (which is used by CRA). https://webpack.js.org/plugins/module-concatenation-plugin/
That would be good, but I was unsure how to do that?
Me neither 😅 Would have to move this rewrite to other phase than transpilation.
apostolos
commented
Jan 17, 2019
I think I've found an edge case: This module defines an FC that passes its The import looked like this: import{memo,useRef,useEffect}from'react';But it breaks like in the first case ( I found the following 3 workarounds: // 1importReact,{useRef,useEffect}from'react';const{ memo }=React;exportconstPortal=memo(/*...*/);// 2importReact,{useRef,useEffect}from'react';exportconstPortal=React.memo(/*...*/);// 3importReact,{memo,useRef,useEffect}from'react';exportconstPortal=memo(/*...render at least some JSX here...*/);Important detail: If |
vincentriemer
commented
Jan 17, 2019
Gave it a try and it appears not to be working with namespace imports. I cloned your branch and added this test: it('should transform React.createElement calls #4',()=>{consttest=` import * as React from "react"; const node = React.createElement("div", null, React.createElement("span", null, "Hello world!")); export function MyComponent() { return node; } `;constoutput=transform(test);expect(output).toMatchSnapshot();});Which results in the following snapshot: exports[`React createElement transforms should transform React.createElement calls #4 1`]=`"import * as React, React from \\"react\\";const node = React.createElement(\\"div\\", null, React.createElement(\\"span\\", null, \\"Hello world!\\"));export function MyComponent() { return node;}"`;Which explains the syntax errors I was getting. |
trueadm
commented
Jan 17, 2019
@apostolos@vincentriemer Thanks for the bug reports. I'll fix them tomorrow. If you want to get involved though – feel free to make a PR against my forked React repro. Any help would be grateful :) |
artembatura
commented
Jan 19, 2019
trueadm
commented
Jan 21, 2019
@artemirq Many of those plugins are still relevant but not in the scope right now for this plugin. We may expand this scope in the future. |
trueadm
commented
Jan 21, 2019
@vincentriemer@apostolos I've released a new version of the plugin to NPM. Please let me know if you find anymore issues. Thanks for the great help! |
apostolos
commented
Jan 21, 2019
@trueadm Fixes all remaining issues for me (also tested |
trueadm
commented
Jan 21, 2019
@apostolos Awesome stuff. Did you notice and differences compared to before (bundle size, performance)? |
Did a quick comparison on lwjgl.org (latest React alpha, uses only function components with hooks, no classes). I've included react-local which does similar optimizations (although, afaik, without the hook transformation): All routes with content
/customize route (more app-like)
EDIT: Source code available here: https://github.com/LWJGL/lwjgl3-www/tree/master/client |
trueadm
commented
Jan 21, 2019
@apostolos Thanks for checking. :) The differences are all very negligible indeed! |
gaearon
commented
Jan 21, 2019
To be fair that example uses TS so I'm not sure it's directly comparable to our current setup. |
chrisvasz
commented
Jan 22, 2019
This is great, but doesn't appear to play nicely with
module.exports={presets: ['@babel/react',['@babel/preset-env',{modules: false,targets: 'ie>=11'}],],plugins: ['optimize-react'],};
importReact,{useState}from'react';functionApp(){let[count,setCount]=useState(0);return<divonClick={()=>setCount(count+1)}>{count}</div>;}output from function_slicedToArray(arr,i){return_arrayWithHoles(arr)||_iterableToArrayLimit(arr,i)||_nonIterableRest();}function_nonIterableRest(){thrownewTypeError("Invalid attempt to destructure non-iterable instance");}function_iterableToArrayLimit(arr,i){var_arr=[];var_n=true;var_d=false;var_e=undefined;try{for(var_i=arr[Symbol.iterator](),_s;!(_n=(_s=_i.next()).done);_n=true){_arr.push(_s.value);if(i&&_arr.length===i)break;}}catch(err){_d=true;_e=err;}finally{try{if(!_n&&_i["return"]!=null)_i["return"]();}finally{if(_d)throw_e;}}return_arr;}function_arrayWithHoles(arr){if(Array.isArray(arr))returnarr;}importReactfrom'react';var__reactCreateElement__=React.createElement;varuseState=React.useState;functionApp(){var_useState=useState(0),_useState2=_slicedToArray(_useState,2),count=_useState2[0],setCount=_useState2[1];return__reactCreateElement__("div",{onClick: functiononClick(){returnsetCount(count+1);}},count);}In this output, it doesn't look like the array destructuring transform happened. If I comment out the line in importReactfrom'react';const__reactCreateElement__=React.createElement;const{
useState
}=React;functionApp(){let_ref_0=useState(0);letsetCount=_ref_0[1];letcount=_ref_0[0];return__reactCreateElement__("div",{onClick: ()=>setCount(count+1)},count);} |
avocadowastaken
commented
Feb 6, 2019
Mostly functional components, tons of hooks. Parsed size become lower, gzipped is bigger: Without `babel-plugin-optimize-react`ParsedGzippedWith `babel-plugin-optimize-react`ParsedGzipped |
trueadm
commented
Feb 6, 2019
@umidbekkarimov Overall that looks to be a general positive win. Gzip size was marginally down but parsed time is where it really went up. Also Brotli compression should further improve over gzip in the cases where the plugin was enabled. |
| const {useState} = React; | ||
| ``` | ||
| ## Array destructuring transform for React's built-in hooks |
There was a problem hiding this comment.
This should be configurable or ideally communicate with targets/browserslists/@babel/preset-env. Array spread syntax is supported by all evergreen browsers and that for quite some time now. The optimization is only interesting if you need to support IE11. See example preset-env without IE11
There was a problem hiding this comment.
We opted to do this because the runtime performance of spread syntax was considerably slower in all browsers when I tests this compared to the transformed version. This might have changed since, as this plugin was created 7 months ago.
There was a problem hiding this comment.
Makes sense. Not sure what the current status of destructuring optimizations in v8 but they had plans to improve it a few months ago. Do you have some numbers about the performance gain?
eps1lon
commented
Jun 5, 2019
-- mui/material-ui#16072 (comment) With |
Friss
commented
Mar 4, 2020
@trueadm Wanted to check in on the status of this getting into CRA. Its certainly an interesting optimization. Also wanted to make a note where we ran into an issue with the usage of a lowercase transforms to |
trueadm
commented
Mar 4, 2020
@Friss I don't believe this will be going in. The gains weren't really there and there wasn't much appetite from folks either. |
gaearon
commented
Apr 15, 2020
Maybe we can just get in the getter fix. https://twitter.com/sebmarkbage/status/1250284377138802689?s=21 |
This PR adds a Babel 7 plugin that aims to optimize certain React patterns that aren't as optimized as they might be. For example, with this plugin the following output is optimized as shown:
Named imports for React get transformed
Array destructuring transform for React's built-in hooks
React.createElement becomes a hoisted constant