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
This commit is contained in:
Spencer Ahrens 2015-12-07 23:08:26 -08:00 committed by facebook-github-bot-0
parent e25d5c2f37
commit 4ce03582a0
1 changed files with 118 additions and 116 deletions

View File

@ -11,22 +11,22 @@
*/ */
'use strict'; 'use strict';
var NativeMethodsMixin = require('NativeMethodsMixin'); const NativeMethodsMixin = require('NativeMethodsMixin');
var Platform = require('Platform'); const Platform = require('Platform');
var React = require('React'); const React = require('React');
var ReactInstanceMap = require('ReactInstanceMap'); const ReactInstanceMap = require('ReactInstanceMap');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); const ReactNativeViewAttributes = require('ReactNativeViewAttributes');
var StyleSheetPropType = require('StyleSheetPropType'); const StyleSheetPropType = require('StyleSheetPropType');
var TextStylePropTypes = require('TextStylePropTypes'); const TextStylePropTypes = require('TextStylePropTypes');
var Touchable = require('Touchable'); const Touchable = require('Touchable');
var createReactNativeComponentClass = const createReactNativeComponentClass =
require('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, { validAttributes: merge(ReactNativeViewAttributes.UIView, {
isHighlighted: true, isHighlighted: true,
numberOfLines: true, numberOfLines: true,
@ -68,10 +68,7 @@ var viewConfig = {
* ``` * ```
*/ */
var Text = React.createClass({ const Text = React.createClass({
mixins: [Touchable.Mixin, NativeMethodsMixin],
propTypes: { propTypes: {
/** /**
* Used to truncate the text with an elipsis after computing the text * Used to truncate the text with an elipsis after computing the text
@ -105,30 +102,106 @@ var Text = React.createClass({
*/ */
allowFontScaling: React.PropTypes.bool, allowFontScaling: React.PropTypes.bool,
}, },
getDefaultProps(): Object {
viewConfig: viewConfig,
getInitialState: function(): Object {
return merge(this.touchableGetInitialState(), {
isHighlighted: false,
});
},
getDefaultProps: function(): Object {
return { return {
accessible: true,
allowFontScaling: true, allowFontScaling: true,
}; };
}, },
getInitialState: function(): Object {
onStartShouldSetResponder: function(): bool { return merge(Touchable.Mixin.touchableGetInitialState(), {
var shouldSetFromProps = this.props.onStartShouldSetResponder && isHighlighted: false,
this.props.onStartShouldSetResponder(); });
return shouldSetFromProps || !!this.props.onPress;
}, },
mixins: [NativeMethodsMixin],
/* viewConfig: viewConfig,
* Returns true to allow responder termination getChildContext(): Object {
return {isInAParentText: true};
},
childContextTypes: {
isInAParentText: React.PropTypes.bool
},
contextTypes: {
isInAParentText: React.PropTypes.bool
},
/**
* Only assigned if touch is needed.
*/ */
handleResponderTerminationRequest: function(): bool { _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 // Allow touchable or props.onResponderTerminationRequest to deny
// the request // the request
var allowTermination = this.touchableHandleResponderTerminationRequest(); var allowTermination = this.touchableHandleResponderTerminationRequest();
@ -137,89 +210,18 @@ var Text = React.createClass({
} }
return allowTermination; 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({ newProps = {
isHighlighted: true, ...this.props,
}); ...this._handlers,
}, isHighlighted: this.state.isHighlighted,
};
touchableHandleActivePressOut: function() {
if (this.props.suppressHighlighting || !this.props.onPress) {
return;
} }
this.setState({ if (this.context.isInAParentText) {
isHighlighted: false, return <RCTVirtualText {...newProps} />;
});
},
touchableHandlePress: function() {
this.props.onPress && this.props.onPress();
},
touchableGetPressRectOffset: function(): RectOffset {
return PRESS_RECT_OFFSET;
},
getChildContext: function(): 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;
}
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} />;
} else { } else {
return <RCTText {...props} />; return <RCTText {...newProps} />;
} }
}, },
}); });