Lazily initialize and cache constant JSX elements (#12967)
Co-authored-by: Justin Ridgewell <justin@ridgewell.name>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { declare } from "@babel/helper-plugin-utils";
|
||||
import { types as t } from "@babel/core";
|
||||
import { types as t, template } from "@babel/core";
|
||||
|
||||
export default declare((api, options) => {
|
||||
api.assertVersion(7);
|
||||
@@ -15,9 +15,33 @@ export default declare((api, options) => {
|
||||
);
|
||||
}
|
||||
|
||||
const HOISTED = new WeakSet();
|
||||
// Element -> Target scope
|
||||
const HOISTED = new WeakMap();
|
||||
|
||||
const immutabilityVisitor = {
|
||||
function declares(node: t.Identifier | t.JSXIdentifier, scope) {
|
||||
if (
|
||||
t.isJSXIdentifier(node, { name: "this" }) ||
|
||||
t.isJSXIdentifier(node, { name: "arguments" }) ||
|
||||
t.isJSXIdentifier(node, { name: "super" }) ||
|
||||
t.isJSXIdentifier(node, { name: "new" })
|
||||
) {
|
||||
const { path } = scope;
|
||||
return path.isFunctionParent() && !path.isArrowFunctionExpression();
|
||||
}
|
||||
|
||||
return scope.hasOwnBinding(node.name);
|
||||
}
|
||||
|
||||
function isHoistingScope({ path }) {
|
||||
return path.isFunctionParent() || path.isLoop() || path.isProgram();
|
||||
}
|
||||
|
||||
function getHoistingScope(scope) {
|
||||
while (!isHoistingScope(scope)) scope = scope.parent;
|
||||
return scope;
|
||||
}
|
||||
|
||||
const analyzer = {
|
||||
enter(path, state) {
|
||||
const stop = () => {
|
||||
state.isImmutable = false;
|
||||
@@ -41,7 +65,8 @@ export default declare((api, options) => {
|
||||
if (
|
||||
path.isJSXIdentifier() ||
|
||||
path.isIdentifier() ||
|
||||
path.isJSXMemberExpression()
|
||||
path.isJSXMemberExpression() ||
|
||||
path.isJSXNamespacedName()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -75,6 +100,90 @@ export default declare((api, options) => {
|
||||
stop();
|
||||
}
|
||||
},
|
||||
|
||||
ReferencedIdentifier(path, state) {
|
||||
const { node } = path;
|
||||
let { scope } = path;
|
||||
|
||||
while (scope) {
|
||||
// We cannot hoist outside of the previous hoisting target
|
||||
// scope, so we return early and we don't update it.
|
||||
if (scope === state.targetScope) return;
|
||||
|
||||
// If the scope declares this identifier (or we're at the function
|
||||
// providing the lexical env binding), we can't hoist the var any
|
||||
// higher.
|
||||
if (declares(node, scope)) break;
|
||||
|
||||
scope = scope.parent;
|
||||
}
|
||||
|
||||
state.targetScope = getHoistingScope(scope);
|
||||
},
|
||||
|
||||
/*
|
||||
See the discussion at https://github.com/babel/babel/pull/12967#discussion_r587948958
|
||||
to uncomment this code.
|
||||
|
||||
ReferencedIdentifier(path, state) {
|
||||
const { node } = path;
|
||||
let { scope } = path;
|
||||
let targetScope;
|
||||
|
||||
let isNestedScope = true;
|
||||
let needsHoisting = true;
|
||||
|
||||
while (scope) {
|
||||
// We cannot hoist outside of the previous hoisting target
|
||||
// scope, so we return early and we don't update it.
|
||||
if (scope === state.targetScope) return;
|
||||
|
||||
// When we hit the scope of our JSX element, we must start
|
||||
// checking if they declare the binding of the current
|
||||
// ReferencedIdentifier.
|
||||
// We don't case about bindings declared in nested scopes,
|
||||
// because the whole nested scope is hoisted alongside the
|
||||
// JSX element so it doesn't impose any extra constraint.
|
||||
if (scope === state.jsxScope) {
|
||||
isNestedScope = false;
|
||||
}
|
||||
|
||||
// If we are in an upper scope and hoisting to this scope has
|
||||
// any benefit, we update the possible targetScope to the
|
||||
// current one.
|
||||
if (!isNestedScope && needsHoisting) {
|
||||
targetScope = scope;
|
||||
}
|
||||
|
||||
// When we start walking in upper scopes, avoid hoisting JSX
|
||||
// elements until we hit a scope introduced by a function or
|
||||
// loop.
|
||||
// This is because hoisting from the inside to the outside
|
||||
// of block or if statements doesn't give any performance
|
||||
// benefit, and it just unnecessarily increases the code size.
|
||||
if (scope === state.jsxScope) {
|
||||
needsHoisting = false;
|
||||
}
|
||||
if (!needsHoisting && isHoistingScope(scope)) {
|
||||
needsHoisting = true;
|
||||
}
|
||||
|
||||
// If the current scope declares the ReferencedIdentifier we
|
||||
// are checking, we break out of this loop. There are two
|
||||
// possible scenarios:
|
||||
// 1. We are in a nested scope, this this declaration means
|
||||
// that this reference doesn't affect the target scope.
|
||||
// The targetScope variable is still undefined.
|
||||
// 2. We are in an upper scope, so this declaration defines
|
||||
// a new hoisting constraint. The targetScope variable
|
||||
// refers to the current scope.
|
||||
if (declares(node, scope)) break;
|
||||
|
||||
scope = scope.parent;
|
||||
}
|
||||
|
||||
if (targetScope) state.targetScope = targetScope;
|
||||
},*/
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -83,30 +192,70 @@ export default declare((api, options) => {
|
||||
visitor: {
|
||||
JSXElement(path) {
|
||||
if (HOISTED.has(path.node)) return;
|
||||
HOISTED.add(path.node);
|
||||
HOISTED.set(path.node, path.scope);
|
||||
|
||||
const state = { isImmutable: true };
|
||||
const name = path.node.openingElement.name;
|
||||
|
||||
// This transform takes the option `allowMutablePropsOnTags`, which is an array
|
||||
// of JSX tags to allow mutable props (such as objects, functions) on. Use sparingly
|
||||
// and only on tags you know will never modify their own props.
|
||||
let mutablePropsAllowed = false;
|
||||
if (allowMutablePropsOnTags != null) {
|
||||
// Get the element's name. If it's a member expression, we use the last part of the path.
|
||||
// So the option ["FormattedMessage"] would match "Intl.FormattedMessage".
|
||||
let namePath = path.get("openingElement.name");
|
||||
while (namePath.isJSXMemberExpression()) {
|
||||
namePath = namePath.get("property");
|
||||
let lastSegment = name;
|
||||
while (t.isJSXMemberExpression(lastSegment)) {
|
||||
lastSegment = lastSegment.property;
|
||||
}
|
||||
|
||||
const elementName = namePath.node.name;
|
||||
state.mutablePropsAllowed =
|
||||
allowMutablePropsOnTags.indexOf(elementName) > -1;
|
||||
const elementName = lastSegment.name;
|
||||
mutablePropsAllowed = allowMutablePropsOnTags.includes(elementName);
|
||||
}
|
||||
|
||||
// Traverse all props passed to this element for immutability.
|
||||
path.traverse(immutabilityVisitor, state);
|
||||
const state = {
|
||||
isImmutable: true,
|
||||
mutablePropsAllowed,
|
||||
targetScope: path.scope.getProgramParent(),
|
||||
};
|
||||
|
||||
if (state.isImmutable) path.hoist();
|
||||
// Traverse all props passed to this element for immutability,
|
||||
// and compute the target hoisting scope
|
||||
path.traverse(analyzer, state);
|
||||
|
||||
if (!state.isImmutable) return;
|
||||
|
||||
const { targetScope } = state;
|
||||
HOISTED.set(path.node, targetScope);
|
||||
|
||||
// In order to avoid hoisting unnecessarily, we need to know which is
|
||||
// the scope containing the current JSX element. If a parent of the
|
||||
// current element has already been hoisted, we can consider its target
|
||||
// scope as the base scope for the current element.
|
||||
let jsxScope;
|
||||
let current = path;
|
||||
while (!jsxScope && current.parentPath.isJSX()) {
|
||||
current = current.parentPath;
|
||||
jsxScope = HOISTED.get(current.node);
|
||||
}
|
||||
jsxScope ??= getHoistingScope(path.scope);
|
||||
|
||||
// Only hoist if it would give us an advantage.
|
||||
if (targetScope === jsxScope) return;
|
||||
|
||||
const id = path.scope.generateUidBasedOnNode(name);
|
||||
targetScope.push({ id: t.identifier(id) });
|
||||
|
||||
let replacement = template.expression.ast`
|
||||
${t.identifier(id)} || (${t.identifier(id)} = ${path.node})
|
||||
`;
|
||||
if (
|
||||
path.parentPath.isJSXElement() ||
|
||||
path.parentPath.isJSXAttribute()
|
||||
) {
|
||||
replacement = t.jsxExpressionContainer(replacement);
|
||||
}
|
||||
|
||||
path.replaceWith(replacement);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user