Permalink
Browse files

Improve Text perf

Summary:
public

Most apps create tons of text components but they are actually quite heavy because of the the Touchable mixin which requires binding tons of functions for every instance created.

This diff makes the binding lazy, so that the main handlers are only bound if there is a valid touch action configured (e.g. onPress), and the Touchable mixin functions are only bound the first time the node is actually touched and becomes the responder.

ScanLab testing shows 5-10% win on render time and memory for various products.

Reviewed By: sebmarkbage

Differential Revision: D2716823

fb-gh-sync-id: 30adb2ed2231c5635c9336369616cf31c776b930
  • Loading branch information...
sahrens authored and facebook-github-bot-0 committed Dec 8, 2015
1 parent e25d5c2 commit 4ce03582a0013e60417dedbf2f760d00e687e540
Showing with 120 additions and 118 deletions.
  1. +120 −118 Libraries/Text/Text.js
View
@@ -11,22 +11,22 @@
*/
'use strict';
-var NativeMethodsMixin = require('NativeMethodsMixin');
-var Platform = require('Platform');
-var React = require('React');
-var ReactInstanceMap = require('ReactInstanceMap');
-var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
-var StyleSheetPropType = require('StyleSheetPropType');
-var TextStylePropTypes = require('TextStylePropTypes');
-var Touchable = require('Touchable');
-
-var createReactNativeComponentClass =
+const NativeMethodsMixin = require('NativeMethodsMixin');
+const Platform = require('Platform');
+const React = require('React');
+const ReactInstanceMap = require('ReactInstanceMap');
+const ReactNativeViewAttributes = require('ReactNativeViewAttributes');
+const StyleSheetPropType = require('StyleSheetPropType');
+const TextStylePropTypes = require('TextStylePropTypes');
+const Touchable = require('Touchable');
+
+const createReactNativeComponentClass =
require('createReactNativeComponentClass');
-var merge = require('merge');
+const merge = require('merge');
-var stylePropType = StyleSheetPropType(TextStylePropTypes);
+const stylePropType = StyleSheetPropType(TextStylePropTypes);
-var viewConfig = {
+const viewConfig = {
validAttributes: merge(ReactNativeViewAttributes.UIView, {
isHighlighted: true,
numberOfLines: true,
@@ -68,10 +68,7 @@ var viewConfig = {
* ```
*/
-var Text = React.createClass({
-
- mixins: [Touchable.Mixin, NativeMethodsMixin],
-
+const Text = React.createClass({
propTypes: {
/**
* Used to truncate the text with an elipsis after computing the text
@@ -105,121 +102,126 @@ var Text = React.createClass({
*/
allowFontScaling: React.PropTypes.bool,
},
-
- viewConfig: viewConfig,
-
- getInitialState: function(): Object {
- return merge(this.touchableGetInitialState(), {
- isHighlighted: false,
- });
- },
- getDefaultProps: function(): Object {
+ getDefaultProps(): Object {
return {
+ accessible: true,
allowFontScaling: true,
};
},
-
- onStartShouldSetResponder: function(): bool {
- var shouldSetFromProps = this.props.onStartShouldSetResponder &&
- this.props.onStartShouldSetResponder();
- return shouldSetFromProps || !!this.props.onPress;
- },
-
- /*
- * Returns true to allow responder termination
- */
- handleResponderTerminationRequest: function(): bool {
- // Allow touchable or props.onResponderTerminationRequest to deny
- // the request
- var allowTermination = this.touchableHandleResponderTerminationRequest();
- if (allowTermination && this.props.onResponderTerminationRequest) {
- allowTermination = this.props.onResponderTerminationRequest();
- }
- return allowTermination;
- },
-
- handleResponderGrant: function(e: SyntheticEvent, dispatchID: string) {
- this.touchableHandleResponderGrant(e, dispatchID);
- this.props.onResponderGrant &&
- this.props.onResponderGrant.apply(this, arguments);
- },
-
- handleResponderMove: function(e: SyntheticEvent) {
- this.touchableHandleResponderMove(e);
- this.props.onResponderMove &&
- this.props.onResponderMove.apply(this, arguments);
- },
-
- handleResponderRelease: function(e: SyntheticEvent) {
- this.touchableHandleResponderRelease(e);
- this.props.onResponderRelease &&
- this.props.onResponderRelease.apply(this, arguments);
- },
-
- handleResponderTerminate: function(e: SyntheticEvent) {
- this.touchableHandleResponderTerminate(e);
- this.props.onResponderTerminate &&
- this.props.onResponderTerminate.apply(this, arguments);
- },
-
- touchableHandleActivePressIn: function() {
- if (this.props.suppressHighlighting || !this.props.onPress) {
- return;
- }
- this.setState({
- isHighlighted: true,
- });
- },
-
- touchableHandleActivePressOut: function() {
- if (this.props.suppressHighlighting || !this.props.onPress) {
- return;
- }
- this.setState({
+ getInitialState: function(): Object {
+ return merge(Touchable.Mixin.touchableGetInitialState(), {
isHighlighted: false,
});
},
-
- touchableHandlePress: function() {
- this.props.onPress && this.props.onPress();
- },
-
- touchableGetPressRectOffset: function(): RectOffset {
- return PRESS_RECT_OFFSET;
- },
-
- getChildContext: function(): Object {
+ mixins: [NativeMethodsMixin],
+ viewConfig: viewConfig,
+ getChildContext(): Object {
return {isInAParentText: true};
},
-
childContextTypes: {
isInAParentText: React.PropTypes.bool
},
-
- render: function() {
- var props = {};
- for (var key in this.props) {
- props[key] = this.props[key];
- }
- // Text is accessible by default
- if (props.accessible !== false) {
- props.accessible = true;
+ contextTypes: {
+ isInAParentText: React.PropTypes.bool
+ },
+ /**
+ * Only assigned if touch is needed.
+ */
+ _handlers: (null: ?Object),
+ /**
+ * These are assigned lazily the first time the responder is set to make plain
+ * text nodes as cheap as possible.
+ */
+ touchableHandleActivePressIn: (null: ?Function),
+ touchableHandleActivePressOut: (null: ?Function),
+ touchableHandlePress: (null: ?Function),
+ touchableGetPressRectOffset: (null: ?Function),
+ render(): ReactElement {
+ let newProps = this.props;
+ if (this.props.onStartShouldSetResponder || this.props.onPress) {
+ if (!this._handlers) {
+ this._handlers = {
+ onStartShouldSetResponder: (): bool => {
+ const shouldSetFromProps = this.props.onStartShouldSetResponder &&
+ this.props.onStartShouldSetResponder();
+ const setResponder = shouldSetFromProps || !!this.props.onPress;
+ if (setResponder && !this.touchableHandleActivePressIn) {
+ // Attach and bind all the other handlers only the first time a touch
+ // actually happens.
+ for (let key in Touchable.Mixin) {
+ if (typeof Touchable.Mixin[key] === 'function') {
+ (this: any)[key] = Touchable.Mixin[key].bind(this);
+ }
+ }
+ this.touchableHandleActivePressIn = () => {
+ if (this.props.suppressHighlighting || !this.props.onPress) {
+ return;
+ }
+ this.setState({
+ isHighlighted: true,
+ });
+ };
+
+ this.touchableHandleActivePressOut = () => {
+ if (this.props.suppressHighlighting || !this.props.onPress) {
+ return;
+ }
+ this.setState({
+ isHighlighted: false,
+ });
+ };
+
+ this.touchableHandlePress = () => {
+ this.props.onPress && this.props.onPress();
+ };
+
+ this.touchableGetPressRectOffset = function(): RectOffset {
+ return PRESS_RECT_OFFSET;
+ };
+ }
+ return setResponder;
+ },
+ onResponderGrant: (e: SyntheticEvent, dispatchID: string) => {
+ this.touchableHandleResponderGrant(e, dispatchID);
+ this.props.onResponderGrant &&
+ this.props.onResponderGrant.apply(this, arguments);
+ },
+ onResponderMove: (e: SyntheticEvent) => {
+ this.touchableHandleResponderMove(e);
+ this.props.onResponderMove &&
+ this.props.onResponderMove.apply(this, arguments);
+ },
+ onResponderRelease: (e: SyntheticEvent) => {
+ this.touchableHandleResponderRelease(e);
+ this.props.onResponderRelease &&
+ this.props.onResponderRelease.apply(this, arguments);
+ },
+ onResponderTerminate: (e: SyntheticEvent) => {
+ this.touchableHandleResponderTerminate(e);
+ this.props.onResponderTerminate &&
+ this.props.onResponderTerminate.apply(this, arguments);
+ },
+ onResponderTerminationRequest: (): bool => {
+ // Allow touchable or props.onResponderTerminationRequest to deny
+ // the request
+ var allowTermination = this.touchableHandleResponderTerminationRequest();
+ if (allowTermination && this.props.onResponderTerminationRequest) {
+ allowTermination = this.props.onResponderTerminationRequest();
+ }
+ return allowTermination;
+ },
+ };
+ }
+ newProps = {
+ ...this.props,
+ ...this._handlers,
+ isHighlighted: this.state.isHighlighted,
+ };
}
- props.isHighlighted = this.state.isHighlighted;
- props.onStartShouldSetResponder = this.onStartShouldSetResponder;
- props.onResponderTerminationRequest =
- this.handleResponderTerminationRequest;
- props.onResponderGrant = this.handleResponderGrant;
- props.onResponderMove = this.handleResponderMove;
- props.onResponderRelease = this.handleResponderRelease;
- props.onResponderTerminate = this.handleResponderTerminate;
-
- // TODO: Switch to use contextTypes and this.context after React upgrade
- var context = ReactInstanceMap.get(this)._context;
- if (context.isInAParentText) {
- return <RCTVirtualText {...props} />;
+ if (this.context.isInAParentText) {
+ return <RCTVirtualText {...newProps} />;
} else {
- return <RCTText {...props} />;
+ return <RCTText {...newProps} />;
}
},
});

1 comment on commit 4ce0358

@qbig

This comment has been minimized.

Show comment
Hide comment
@qbig

qbig Dec 14, 2015

nice!

qbig commented on 4ce0358 Dec 14, 2015

nice!

Please sign in to comment.