diff --git a/src/babel/transformation/transformers/index.js b/src/babel/transformation/transformers/index.js
index 7b169e67a2..3b7cbbc842 100644
--- a/src/babel/transformation/transformers/index.js
+++ b/src/babel/transformation/transformers/index.js
@@ -18,6 +18,7 @@ export default {
"spec.blockScopedFunctions": require("./spec/block-scoped-functions"),
+ "optimisation.react.constantElements": require("./optimisation/react.constant-elements"),
reactCompat: require("./other/react-compat"),
react: require("./other/react"),
diff --git a/src/babel/transformation/transformers/optimisation/react.constant-elements.js b/src/babel/transformation/transformers/optimisation/react.constant-elements.js
new file mode 100644
index 0000000000..03f1524194
--- /dev/null
+++ b/src/babel/transformation/transformers/optimisation/react.constant-elements.js
@@ -0,0 +1,42 @@
+import * as react from "../../helpers/react";
+
+export var metadata = {
+ optional: true
+};
+
+var immutabilityVisitor = {
+ enter(node, parent, scope, state) {
+ var stop = () => {
+ state.isImmutable = false;
+ this.stop();
+ };
+
+ if (this.isJSXClosingElement()) {
+ this.skip();
+ return;
+ }
+
+ if (this.isJSXIdentifier({ name: "ref" }) && this.parentPath.isJSXAttribute({ name: node })) {
+ return stop();
+ }
+
+ if (this.isJSXIdentifier() || this.isIdentifier() || this.isJSXMemberExpression()) {
+ return;
+ }
+
+ if (!this.isImmutable()) stop();
+ }
+};
+
+export function JSXElement(node, parent, scope, file) {
+ if (node._ignoreConstant) return;
+
+ var state = { isImmutable: true };
+ this.traverse(immutabilityVisitor, state);
+ this.skip();
+
+ if (state.isImmutable) {
+ this.hoist();
+ node._ignoreConstant = true;
+ }
+}
diff --git a/src/babel/traversal/path/hoister.js b/src/babel/traversal/path/hoister.js
new file mode 100644
index 0000000000..e47c971051
--- /dev/null
+++ b/src/babel/traversal/path/hoister.js
@@ -0,0 +1,106 @@
+import * as react from "../../transformation/helpers/react";
+import * as t from "../../types";
+
+var referenceVisitor = {
+ enter(node, parent, scope, state) {
+ if (this.isJSXIdentifier() && react.isCompatTag(node.name)) {
+ return;
+ }
+
+ if (this.isJSXIdentifier() || this.isIdentifier()) {
+ // direct references that we need to track to hoist this to the highest scope we can
+ if (this.isReferenced()) {
+ var bindingInfo = scope.getBinding(node.name);
+
+ if (bindingInfo && bindingInfo.constant) {
+ state.bindings[node.name] = bindingInfo;
+ } else {
+ scope.dump();
+ state.foundIncompatible = true;
+ this.stop();
+ }
+ }
+ }
+ }
+};
+
+export default class PathHoister {
+ constructor(path) {
+ this.foundIncompatible = false;
+ this.bindings = {};
+ this.scopes = [];
+ this.path = path;
+ }
+
+ isCompatibleScope(scope) {
+ for (var key in this.bindings) {
+ var binding = this.bindings[key];
+ if (!scope.bindingIdentifierEquals(key, binding.identifier)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ getCompatibleScopes() {
+ var checkScope = this.path.scope;
+ do {
+ if (this.isCompatibleScope(checkScope)) {
+ this.scopes.push(checkScope);
+ } else {
+ break;
+ }
+ } while(checkScope = checkScope.parent);
+ }
+
+ getAttachmentPath() {
+ var scopes = this.scopes;
+
+ var scope = scopes.pop();
+
+ if (scope.path.isFunction()) {
+ if (this.hasNonParamBindings()) {
+ // can't be attached to this scope
+ return this.getNextScopeStatementParent();
+ } else {
+ // needs to be attached to the body
+ return scope.path.get("body").get("body")[0];
+ }
+ } else if (scope.path.isProgram()) {
+ return this.getNextScopeStatementParent();
+ }
+ }
+
+ getNextScopeStatementParent() {
+ var scope = this.scopes.pop();
+ if (scope) return scope.path.getStatementParent();
+ }
+
+ hasNonParamBindings() {
+ for (var name in this.bindings) {
+ var binding = this.bindings[name];
+ if (binding.kind !== "param") return true;
+ }
+ return false;
+ }
+
+ run() {
+ this.path.traverse(referenceVisitor, this);
+ if (this.foundIncompatible) return;
+
+ this.getCompatibleScopes();
+
+ var path = this.getAttachmentPath();
+ if (!path) return;
+
+ var uid = path.scope.generateUidIdentifier("ref");
+
+ path.insertBefore([
+ t.variableDeclaration("var", [
+ t.variableDeclarator(uid, this.path.node)
+ ])
+ ]);
+
+ this.path.replaceWith(uid);
+ }
+}
diff --git a/src/babel/traversal/path/index.js b/src/babel/traversal/path/index.js
index 54663b5224..46d08294b0 100644
--- a/src/babel/traversal/path/index.js
+++ b/src/babel/traversal/path/index.js
@@ -635,6 +635,14 @@ export default class TraversalPath {
traverse(opts, state) {
traverse(this.node, opts, this.scope, state, this);
+
+ /**
+ * Description
+ */
+
+ hoist() {
+ var hoister = new PathHoister(this);
+ return hoister.run();
}
/**
diff --git a/src/babel/types/validators.js b/src/babel/types/validators.js
index 20e458cbd6..d532bfaa3b 100644
--- a/src/babel/types/validators.js
+++ b/src/babel/types/validators.js
@@ -156,11 +156,11 @@ export function isSpecifierDefault(specifier: Object): boolean {
export function isScope(node: Object, parent: Object): boolean {
if (t.isBlockStatement(node)) {
- if (t.isLoop(parent.block, { body: node })) {
+ if (t.isLoop(parent, { body: node })) {
return false;
}
- if (t.isFunction(parent.block, { body: node })) {
+ if (t.isFunction(parent, { body: node })) {
return false;
}
}
diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/constructor/actual.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/constructor/actual.js
new file mode 100644
index 0000000000..d8517281fc
--- /dev/null
+++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/constructor/actual.js
@@ -0,0 +1,5 @@
+var Foo = require("Foo");
+
+function render() {
+ return