-
-
Notifications
You must be signed in to change notification settings - Fork 8
Description
In order to optimize View components, we need to ensure that they are not wrapped inside a Text component. Currently, we only check within the current file's context, if there's a Text ancestor component (direct or indirect, i.e. within a custom component – as long as it's defined within the same file):
react-native-boost/packages/react-native-boost/src/plugin/optimizers/view/index.ts
Lines 70 to 158 in a8d5ced
| /** | |
| * Returns true if any ancestor element is a <Text /> or contains a <Text />. | |
| * This function handles both direct Text ancestors and custom components that may contain Text. | |
| * TODO: We can't test across file boundaries within the Babel plugin | |
| */ | |
| function hasTextAncestor(path: NodePath<t.JSXOpeningElement>): boolean { | |
| // Check for direct Text ancestors (no custom components) | |
| const directTextAncestor = path.findParent((parentPath) => { | |
| return t.isJSXElement(parentPath.node) && t.isJSXIdentifier(parentPath.node.openingElement.name, { name: 'Text' }); | |
| }); | |
| if (directTextAncestor) return true; | |
| // Check for indirect Text ancestors (custom components that contain Text) | |
| return !!path.findParent((parentPath) => { | |
| // Only check JSX elements | |
| if (!t.isJSXElement(parentPath.node)) return false; | |
| // Get the component name | |
| const openingElement = parentPath.node.openingElement; | |
| if (!t.isJSXIdentifier(openingElement.name)) return false; | |
| const componentName = openingElement.name.name; | |
| // Skip built-in components and already checked Text component | |
| if ( | |
| componentName === 'Text' || | |
| componentName === 'View' || | |
| componentName === 'Fragment' || | |
| componentName[0] === componentName[0].toLowerCase() | |
| ) { | |
| return false; | |
| } | |
| // Try to find the component definition through variable binding | |
| const binding = parentPath.scope.getBinding(componentName); | |
| if (!binding) return false; | |
| // Now check the component definition for Text elements | |
| if (t.isVariableDeclarator(binding.path.node)) { | |
| const init = binding.path.node.init; | |
| // Handle arrow functions or function expressions | |
| if (t.isArrowFunctionExpression(init) || t.isFunctionExpression(init)) { | |
| // Check the function body for Text elements | |
| return t.isBlockStatement(init.body) ? hasTextInReturnStatement(init.body) : hasTextInExpression(init.body); | |
| } | |
| } else if (t.isFunctionDeclaration(binding.path.node)) { | |
| // Handle function declarations | |
| return hasTextInReturnStatement(binding.path.node.body); | |
| } | |
| return false; | |
| }); | |
| } | |
| /** | |
| * Check if a block statement contains a return statement with a Text element | |
| */ | |
| function hasTextInReturnStatement(blockStatement: t.BlockStatement): boolean { | |
| for (const statement of blockStatement.body) { | |
| if (t.isReturnStatement(statement) && statement.argument && hasTextInExpression(statement.argument)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Check if an expression contains a Text element | |
| */ | |
| function hasTextInExpression(expression: t.Expression): boolean { | |
| // If directly returning a JSX element | |
| if (t.isJSXElement(expression)) { | |
| // Check if it's a Text element | |
| if (t.isJSXIdentifier(expression.openingElement.name, { name: 'Text' })) { | |
| return true; | |
| } | |
| // Check if any children are Text elements | |
| for (const child of expression.children) { | |
| if (t.isJSXElement(child) && t.isJSXIdentifier(child.openingElement.name, { name: 'Text' })) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } |
However, we don't have a way of checking that across file boundaries yet. For example, this will currently optimize the View component, even though it shouldn't:
// CustomComponent.tsx
import { Text } from 'react-native';
export function CustomComponent() {
return (
<Text>{children}</Text>
);
}
// SomeScreen.tsx
import { View } from 'react-native';
import { CustomComponent } from './CustomComponent';
export default function CustomComponent() {
return (
<CustomComponent>
<View /> {/* This should not be optimized! */}
</CustomComponent>
);
}Discussion
This is likely out of scope for a Babel plugin. Babel only looks at one file at a time and – as far as I know – does not have the capabilities for a full-fledged module graph. The only two possibilities would therefore be growing out of the Babel plugin or disabling optimizations of Views with ancestors that can't be resolved from the current file, and putting it behind a configuration flag like __dangerouslyOptimizeViewWithCrossFileAncestor.
Workaround
To prevent issues, manually exclude either the file or the line from the optimization. For example:
// SomeScreen.tsx
import { CustomComponent } from './CustomComponent';
export default function CustomComponent() {
return (
<CustomComponent>
{/* @boost-ignore */}
<View />
</CustomComponent>
);
}Alternatively, disable the whole View optimizer (as seen in the benchmarks, the Text optimizer is far more important when it comes to improving performance):
// babel.config.js
module.exports = {
plugins: [
[
'react-native-boost/plugin',
{
optimizers: {
view: false,
},
},
],
],
};