-
Notifications
You must be signed in to change notification settings - Fork 47.1k
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
Optimizing Compiler: Reuse Constant Value Types like ReactElement #3226
Comments
cc @sebmck |
This is great and exactly the type of thing I had in mind with babel/babel#653. Babel has pretty strong scope and reassignment tracking so these types of optimisations should be relatively straightforward. |
If the optimization in #3228 is applied, then the children array and the props object itself might be reused independently from the element itself. render() {
return <div className={this.props.className}>hello <span>world</span></div>;
} var children = ['hello', { type: 'span', props: { children: 'world' }, key: null, ref: null }];
render() {
return { type: 'div', props: { className: this.props.className, children: children }, key: null, ref: null };
} This can be useful for reusing empty props objects: render(someKey) {
return <Foo key={someKey} />;
} var emptyProps = {};
render(someKey) {
return { type: 'div', props: emptyProps, key: someKey, ref: null };
} |
Did some initial prototyping in babel/babel@2703338. Pretty rough and there are quite a few edgecases I can think of that it doesn't handle but it's a good start. function render() {
return <div className="foo" />;
} becomes "use strict";
var _ref = React.createElement("div", { className: "foo" });
function render() {
return _ref;
} and var Foo = require('Foo');
function createComponent(text) {
return function render() {
return <Foo>{text}</Foo>;
};
} becomes "use strict";
var Foo = require("Foo");
function createComponent(text) {
var _ref = React.createElement(Foo, null, text);
return function render() {
return _ref;
};
} |
nice! |
This is an optimization that affects run-time behavior though, right? |
That is only because we don't have the distinction between value equality and physical equality in JavaScript. That's what we're trying to change. https://github.com/sebmarkbage/ecmascript-immutable-data-structures You're very unlikely to rely on different referential equality of two otherwise equivalent React Elements. I've never seen a case for it, unless you have a mutable nested object. In which case this optimization should not apply. |
This is an optimization that should ideally be a applied in development mode too, just to be safe. |
I realized that if you have the code: var CurrentTime = React.createClass({
render: function() {
return <div>{"" + new Date()}</div>;
}
})
var Clock = React.createClass({
componentDidMount: function() {
this.interval = setInterval(() => this.forceUpdate(), 1000);
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
render: function() {
return (
<div>Right now, its: <CurrentTime /></div>
);
}
});
React.render(<Clock />, document.body); then currently this shows an autoupdating time, but would not with this optimization. :\ Wonder if we can come up with some way to catch this in development (i.e., look for DOM mutations that happen?) and warn or something. |
@spicyj : I think the definition of constant values has to be that the expression contains zero references to non-local bindings and zero side-effectful expressions (member expressions, function calls, etc). So with that assumption, we're fine here because |
No, @spicyj is right because of how the reconciliation bails out by default if the element is reused. However, relying on global state without a forceUpdate is not a supported use case. The solution is to add a forceUpdate on a timer in the deeper component.
|
I guess the difference is that before, if you didn't do a forceUpdate() (i.e. you didn't comply) you'd just lose out on a re-render...whereas with this optimization, you could actually be breaking more than just UI rendering |
Well, it is still just UI rendering, but yea, this is an added risk. We make the following two assumptions:
We don't have have runtime or static type system support for validating those things yet. We have to rely on convention to enforce those constraints. If you violate those, you're SOL. However, this optimization is not the only place you'll risk getting screwed. It is easy to shoot yourself in the foot in other places too if you violate these principals. We'll just have to make harder to bring value types and pure functions to the runtime and Flow to make it easier to ensure that you're compliant. No need to punish compliant code while we wait. We've had this idea of doing pseudo-random double rendering in |
I'd like to propose moving the initialization for static elements into either componentWillMount or the constructor, and caching them on the module level. Taking out all of the static elements of a huge React app would add a significant initialization time overhead and also initialize components that might not be rendered at all (imagine downloading the full JS for the App but only rendering one view at a time). I propose doing this optimization for React.createClass and classes that extend // before
class MyComponent extends React.Component {
render: function() {
return <div className="foo" />;
}
}
// after
var _ref;
class MyComponent extends React.Component {
constructor(props, context) {
super(props, context),
_ref = _ref || React.createElement("div", { className: "foo" });
},
render: function() {
return _ref;
}
} Similarly, for a |
@cpojer Should probably make it |
oh yes, of course! Edited my example. It could also of course have a single check for all of the static components in one module, like |
A simpler transform would be to do var _ref;
class MyComponent extends React.Component {
render: function() {
return _ref || (_ref = <div className="foo" />);
}
} which might be a little slower than your proposal but much easier to transform. |
@cpojer That is a much more specific optimization that ties it specifically to assumptions about React classes. I don't think we're ready for that yet. See Advanced Optimizations for other use cases. This optimization is really a generic one that is employed by functional engines to any value type all over. It is also generalizable to the value types proposal for ES7. We should really hold off on the React specific ones. I'd also argue that you don't need your React class if you don't intend to call render on it yet. So the whole module and class can be deferred. That's a much more generic optimization that can be used for a lot of things. But it separate from this issue. |
Is there a reason however not to include this in the transform? I don't think it would make the transform that much harder to build – I'm not proposing only allowing the general optimization for React components, but have a separate optimization that makes this work better for React components. |
@cpojer It ties it to subtle differences in React semantics that we can't prove are sound. For example, Besides, you have to allocate the binding slot for the variable regardless. I think we're better off investing in more generic optimizations such as lazy initialization of the entire module body. If we need statics, then we can pull them apart from the main classes. For example: // foo.js
export class Foo {
render() {
}
}
Foo.myConst = 10; // bar.js
import { Foo } from "foo";
now(Foo.myConst);
function later() {
new Foo();
} Could be transformed into (something equivalent to): // foo-static-content.js
export myConst = 10; // foo-deferred-content.js
export class Foo {
render() {
}
} // bar.js
import { myConst } from "foo-static-content";
now(myConst);
function later() {
new (require("foo-deferred-content"))();
} |
…compatible scope, a compatible scope is considered to be one where all references inside can be resolved to, also adds an optimisation.react.constantElements transformer that uses this to much success facebook/react#3226
I've done a complete and stable implementation of this (see attached commit). Still need to add a lot more tests so I can be absolutely confident in it's reliability. |
Just an update but the transformer is now smarter in it's hoisting and no longer deopts completely on reassigned bindings. For example: function renderName(firstName) {
firstName += " (inactive)";
return function () {
return <span>{firstName}</span>;
};
} becomes "use strict";
function renderName(firstName) {
firstName += " (inactive)";
var _ref = React.createElement(
"span",
null,
firstName
);
return function () {
return _ref;
};
} and no longer bails because of |
@sebmarkbage |
Babel supports this in conjunction with React 0.14. Enable the |
Is this transformation potentially dangerous? |
What happens when you try it? |
I was deceived that transformer can hoist constant element when it shouldn't but after some time playing with my dispute opponent's examples I realised transform works properly. Sorry for false alarm =) |
JSX support By popular demand... This implements very basic support for JSX. Still to do: * Support pragmas other than `React.createElement` * Optimisations (see [here](https://medium.com/doctolib-engineering/improve-react-performance-with-babel-16f1becfaa25#.thsp2ymcd) and [here](facebook/react#3226)) * Some method to auto-import needed modules? (e.g. automatically add `import * as React from 'react'` It's still an open question whether this truly belongs in core, but I do like the convenience of it. Since I don't ever use JSX, I could have completely pooched this up – would welcome input from people more experienced with it. See merge request !37
I see this issue is closed but cannot find a better place to post this. I have developed a small module https://www.npmjs.com/package/react-cache which handles caching React elements with variable props. It is a declarative way to replace shouldComponentUpdate for now. |
I think @sophiebits’s example in #3226 (comment) also presents a less obvious place where it could break: class Parent extends React.Component {
state = { person: { name: 'Dan' } };
cachedEl = <Child person={this.state.person} />;
handleClick = () => {
this.state.person.name = 'Dominic';
this.forceUpdate(); // Or even this.setState({})
}
render() {
return (
<div onClick={this.handleClick}>
{this.cachedEl}
<Child person={this.state.person} />
</div>
);
}
}
function Child(props) {
return <h1>Hi, {props.name}!</h1>;
} In this example, one |
I don't understand this sentence. Are mutable props supported ? Isn't it bad practice that should be avoided at all cost ? |
It's not nice but it's supported. I'm pointing out a case that works in React but breaks with this optimization. |
Specifically, note that I'm not mutating props but I am mutating some object in the state, and then calling forceUpdate. This has always been supported as it's often the only way to start integrating a legacy codebase with React. |
@gaearon . Hi! function Child(props) {
return <h1>Hi, {props.name}!</h1>; // it should be props.person.name
} right e.g code: function Child(props) {
return <h1>Hi, {props.person.name}!</h1>;
} |
JSX support By popular demand... This implements very basic support for JSX. Still to do: * Support pragmas other than `React.createElement` * Optimisations (see [here](https://medium.com/doctolib-engineering/improve-react-performance-with-babel-16f1becfaa25#.thsp2ymcd) and [here](facebook/react#3226)) * Some method to auto-import needed modules? (e.g. automatically add `import * as React from 'react'` It's still an open question whether this truly belongs in core, but I do like the convenience of it. Since I don't ever use JSX, I could have completely pooched this up – would welcome input from people more experienced with it. See merge request !37
JSX support By popular demand... This implements very basic support for JSX. Still to do: * Support pragmas other than `React.createElement` * Optimisations (see [here](https://medium.com/doctolib-engineering/improve-react-performance-with-babel-16f1becfaa25#.thsp2ymcd) and [here](facebook/react#3226)) * Some method to auto-import needed modules? (e.g. automatically add `import * as React from 'react'` It's still an open question whether this truly belongs in core, but I do like the convenience of it. Since I don't ever use JSX, I could have completely pooched this up – would welcome input from people more experienced with it. See merge request !37
JSX support By popular demand... This implements very basic support for JSX. Still to do: * Support pragmas other than `React.createElement` * Optimisations (see [here](https://medium.com/doctolib-engineering/improve-react-performance-with-babel-16f1becfaa25#.thsp2ymcd) and [here](facebook/react#3226)) * Some method to auto-import needed modules? (e.g. automatically add `import * as React from 'react'` It's still an open question whether this truly belongs in core, but I do like the convenience of it. Since I don't ever use JSX, I could have completely pooched this up – would welcome input from people more experienced with it. See merge request !37
Starting with 0.14 we will be able to start treating ReactElements and their props objects as value types. I.e. any instance is conceptually equivalent if all their values are the same. This allow us to reuse any ReactElement whose inputs are deeply immutable or effectively constant.
Take this function for example:
This can be optimized by moving the JSX out of the function body so that each time it is called the same instance is returned:
Not only does it allow us to reuse the same objects, React will automatically bail out any reconciliation of constant components - without a manual
shouldComponentUpdate
.Reference Equality
Objects in JavaScript have reference equality. Meaning that this optimization can actually change behavior of code. If any of your calls to render() uses object equality or uses the ReactElement as the key in a Map, this optimization will break that use case. So don't rely on that.
This is a change in the semantic contract of ReactElements. This is difficult to enforce, but hopefully a future version of JavaScript will have the notion of value equality for custom objects so this can be enforced.
What is Constant?
The simplest assumption is if the entire expression, including all the props (and children), are all literal value types (strings, booleans, null, undefined or JSX), then the result is constant.
If a variable is used in the expression, then you must first ensure that it is not ever mutated since then the timing can affect the behavior.
It is safe to move a constant to a higher closure if the variable is never mutated. You can only move it to a scope that is shared by all variables.
Are Objects Constant?
Arbitrary objects are not considered constant. A transpiler should NEVER move a ReactElement scope if any of the parameters is a mutable object. React will silently ignore updates and it will change behavior.
If an object is provably deeply immutable (or effectively immutable by never being mutated), the transpiler may only move it to the scope where the object was created or received.
This is due to the fact that arbitrary objects have referential identity in JavaScript. However, if the semantics of an immutable object is expected to have value equality, it might be ok to treat them as value types. For example any data structure created by immutable-js may be treated as a value type if it is deeply immutable.
Exception: ref="string"
There is unfortunately one exception. If the
ref
prop might potentially might have a string value. Then it is never safe to reuse the element. This is due to the fact that we capture the React owner at the time of creation. This is an unfortunate artifact and we're looking at various options of changing the refs semantics to fix it.Non-JSX
This can work on JSX, React.createElement or functions created by React.createFactory.
For example, it is safe to assume that this function call generates a constant ReactElement.
Therefore it is safe to reuse:
Advanced Optimizations
You can also imagine even more clever optimizations that optimize per-instance elements by memoizing it on the instance. This allows auto-bound methods to be treated as effectively constant.
If you can track pure-functions, you can even treat calculated values as constants if the input to the pure function is constant.
Static analysis tools like Flow makes it possible to detect that even more elements are constant.
The text was updated successfully, but these errors were encountered: