From 976320916e3c7db3c24428fe33f72b4d35c4d4ee Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Sat, 18 Mar 2017 17:27:53 -0700 Subject: [PATCH] [change] move bridge code into createDOMElement Moves event normalization and the ResponderEventPlugin injection from 'View' to 'createDOMElement'. The 'react-native-web/lite' variant is removed from the performance directory as the implementation is not substantially different. Micro-optimizations to marginally narrow the performance gap to css-modules. --- performance/README.md | 21 +++----- .../react-native-web/Box/lite.js | 49 ------------------- .../react-native-web/View/lite.js | 32 ------------ .../implementations/react-native-web/lite.js | 7 --- performance/index.js | 3 -- src/components/View/index.js | 49 ++----------------- src/modules/createDOMElement/index.js | 46 ++++++++++++++++- 7 files changed, 56 insertions(+), 151 deletions(-) delete mode 100644 performance/implementations/react-native-web/Box/lite.js delete mode 100644 performance/implementations/react-native-web/View/lite.js delete mode 100644 performance/implementations/react-native-web/lite.js diff --git a/performance/README.md b/performance/README.md index ddd3fb7c..74b78aaa 100644 --- a/performance/README.md +++ b/performance/README.md @@ -10,25 +10,18 @@ open ./performance/index.html ## Notes The components used in the render benchmarks are simple enough to be -implemented by multiple styling libraries. The implementations are not -equivalent but are useful for framing the relative performance of -`react-native-web` against these tests. - -The implementations are not equivalent. For example, the `react-native-web` -implementation of `View` does more than just styling. The -`react-native-web/lite` variant implements a minimal `View` that allows for a -more direct comparison with the `css-modules` baseline. +implemented by multiple UI or style libraries. The implementations are not +equivalent in functionality. ## Benchmark results -Typical render timings*: mean / two standard deviations +Typical render timings*: mean ± two standard deviations | Implementation | Deep tree (ms) | Wide tree (ms) | | :--- | ---: | ---: | -| css-modules | `75.40` `±15.93` | `162.15` `±22.20` | -| react-native-web/lite@0.0.77 | `83.93` `±13.80` | `177.57` `±20.045` | -| react-native-web@0.0.77 | `106.72` `±15.48` | `217.63` `±25.70` | -| styled-components@2.0.0-7 | `255.19` `±35.09` | `569.74` `±59.94` | -| glamor@3.0.0-1 | `268.94` `±38.96` | `458.69` `±32.30` | +| `css-modules` | `76.66` `±18.46` | `157.03` `±19.79` | +| `react-native-web@0.0.78` | `90.13` `±20.91` | `198.72` `±24.44` | +| `styled-components@2.0.0-7` | `263.06` `±31.87` | `564.53` `±27.62` | +| `glamor@3.0.0-1` | `267.49` `±35.12` | `451.99` `±37.32` | *MacBook Pro (13-inch, Early 2011); 2.7 GHz Intel Core i7; 16 GB 1600 MHz DDR3. Google Chrome 56. diff --git a/performance/implementations/react-native-web/Box/lite.js b/performance/implementations/react-native-web/Box/lite.js deleted file mode 100644 index 47ad5e98..00000000 --- a/performance/implementations/react-native-web/Box/lite.js +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from 'react'; -import StyleSheet from 'react-native/apis/StyleSheet'; -import View from '../View/lite'; - -const Box = ({ color, fixed = false, layout = 'column', outer = false, ...other }) => ( - -); - -const styles = StyleSheet.create({ - outer: { - padding: 4 - }, - row: { - flexDirection: 'row' - }, - color0: { - backgroundColor: '#222' - }, - color1: { - backgroundColor: '#666' - }, - color2: { - backgroundColor: '#999' - }, - color3: { - backgroundColor: 'blue' - }, - color4: { - backgroundColor: 'orange' - }, - color5: { - backgroundColor: 'red' - }, - fixed: { - width: 20, - height: 20 - } -}); - -module.exports = Box; diff --git a/performance/implementations/react-native-web/View/lite.js b/performance/implementations/react-native-web/View/lite.js deleted file mode 100644 index 013ee36a..00000000 --- a/performance/implementations/react-native-web/View/lite.js +++ /dev/null @@ -1,32 +0,0 @@ -import createDOMElement from 'react-native/modules/createDOMElement'; -import StyleSheet from 'react-native/apis/StyleSheet'; - -const View = props => createDOMElement('div', { ...props, style: [styles.initial, props.style] }); - -const styles = StyleSheet.create({ - initial: { - alignItems: 'stretch', - borderWidth: 0, - borderStyle: 'solid', - boxSizing: 'border-box', - display: 'flex', - flexBasis: 'auto', - flexDirection: 'column', - margin: 0, - padding: 0, - position: 'relative', - // button and anchor reset - backgroundColor: 'transparent', - color: 'inherit', - font: 'inherit', - textAlign: 'inherit', - textDecorationLine: 'none', - // list reset - listStyle: 'none', - // fix flexbox bugs - minHeight: 0, - minWidth: 0 - } -}); - -module.exports = View; diff --git a/performance/implementations/react-native-web/lite.js b/performance/implementations/react-native-web/lite.js deleted file mode 100644 index 94249602..00000000 --- a/performance/implementations/react-native-web/lite.js +++ /dev/null @@ -1,7 +0,0 @@ -import Box from './Box/lite'; -import View from './View/lite'; - -export default { - Box, - View -}; diff --git a/performance/index.js b/performance/index.js index 1704a55e..9b213c60 100644 --- a/performance/index.js +++ b/performance/index.js @@ -1,7 +1,6 @@ import cssModules from './implementations/css-modules'; import glamor from './implementations/glamor'; import reactNative from './implementations/react-native-web'; -import reactNativeLite from './implementations/react-native-web/lite'; import styledComponents from './implementations/styled-components'; import renderDeepTree from './tests/renderDeepTree'; @@ -10,13 +9,11 @@ import renderWideTree from './tests/renderWideTree'; const tests = [ // deep tree () => renderDeepTree('css-modules', cssModules), - () => renderDeepTree('react-native-web/lite', reactNativeLite), () => renderDeepTree('react-native-web', reactNative), () => renderDeepTree('styled-components', styledComponents), () => renderDeepTree('glamor', glamor), // wide tree () => renderWideTree('css-modules', cssModules), - () => renderWideTree('react-native-web/lite', reactNativeLite), () => renderWideTree('react-native-web', reactNative), () => renderWideTree('styled-components', styledComponents), () => renderWideTree('glamor', glamor) diff --git a/src/components/View/index.js b/src/components/View/index.js index 0a9c1932..90983d51 100644 --- a/src/components/View/index.js +++ b/src/components/View/index.js @@ -1,49 +1,11 @@ -import '../../modules/injectResponderEventPlugin'; - import applyLayout from '../../modules/applyLayout'; import applyNativeMethods from '../../modules/applyNativeMethods'; import createDOMElement from '../../modules/createDOMElement'; -import normalizeNativeEvent from '../../modules/normalizeNativeEvent'; import StyleSheet from '../../apis/StyleSheet'; import ViewPropTypes from './ViewPropTypes'; import { Component, PropTypes } from 'react'; -const eventHandlerNames = [ - 'onClick', - 'onClickCapture', - 'onMoveShouldSetResponder', - 'onMoveShouldSetResponderCapture', - 'onResponderGrant', - 'onResponderMove', - 'onResponderReject', - 'onResponderRelease', - 'onResponderTerminate', - 'onResponderTerminationRequest', - 'onStartShouldSetResponder', - 'onStartShouldSetResponderCapture', - 'onTouchCancel', - 'onTouchCancelCapture', - 'onTouchEnd', - 'onTouchEndCapture', - 'onTouchMove', - 'onTouchMoveCapture', - 'onTouchStart', - 'onTouchStartCapture' -]; - -const _normalizeEventForHandler = handler => e => { - e.nativeEvent = normalizeNativeEvent(e.nativeEvent); - return handler(e); -}; - -const normalizeEventHandlers = props => { - eventHandlerNames.forEach(handlerName => { - const handler = props[handlerName]; - if (typeof handler === 'function') { - props[handlerName] = _normalizeEventForHandler(handler); - } - }); -}; +const emptyObject = {}; class View extends Component { static displayName = 'View'; @@ -63,9 +25,9 @@ class View extends Component { }; getChildContext() { - return { - isInAButtonView: this.props.accessibilityRole === 'button' - }; + const isInAButtonView = this.props.accessibilityRole === 'button' || + this.context.isInAButtonView; + return isInAButtonView ? { isInAButtonView } : emptyObject; } render() { @@ -87,9 +49,6 @@ class View extends Component { const component = this.context.isInAButtonView ? 'span' : 'div'; - // DOM events need to be normalized to expect RN format - normalizeEventHandlers(otherProps); - otherProps.style = [styles.initial, style, pointerEvents && pointerEventStyles[pointerEvents]]; return createDOMElement(component, otherProps); diff --git a/src/modules/createDOMElement/index.js b/src/modules/createDOMElement/index.js index 17661418..049bba94 100644 --- a/src/modules/createDOMElement/index.js +++ b/src/modules/createDOMElement/index.js @@ -1,3 +1,6 @@ +import '../injectResponderEventPlugin'; + +import normalizeNativeEvent from '../normalizeNativeEvent'; import React from 'react'; import StyleRegistry from '../../apis/StyleSheet/registry'; @@ -19,23 +22,64 @@ const roleComponents = { region: 'section' }; +const eventHandlerNames = { + onClick: true, + onClickCapture: true, + onMoveShouldSetResponder: true, + onMoveShouldSetResponderCapture: true, + onResponderGrant: true, + onResponderMove: true, + onResponderReject: true, + onResponderRelease: true, + onResponderTerminate: true, + onResponderTerminationRequest: true, + onStartShouldSetResponder: true, + onStartShouldSetResponderCapture: true, + onTouchCancel: true, + onTouchCancelCapture: true, + onTouchEnd: true, + onTouchEndCapture: true, + onTouchMove: true, + onTouchMoveCapture: true, + onTouchStart: true, + onTouchStartCapture: true +}; + +const wrapEventHandler = handler => e => { + e.nativeEvent = normalizeNativeEvent(e.nativeEvent); + return handler(e); +}; + const createDOMElement = (component, rnProps) => { const { accessibilityLabel, accessibilityLiveRegion, accessibilityRole, accessible = true, - style: rnStyle, // we need to remove the RN styles from 'domProps' + style: rnStyle, testID, type, ...domProps } = rnProps || emptyObject; + // use equivalent platform elements where possible const accessibilityComponent = accessibilityRole && roleComponents[accessibilityRole]; const Component = accessibilityComponent || component; + // convert React Native styles to DOM styles const { className, style } = StyleRegistry.resolve(rnStyle) || emptyObject; + // normalize DOM events to match React Native events + // TODO: move this out of the render path + for (const prop in domProps) { + if (Object.prototype.hasOwnProperty.call(domProps, prop)) { + const isEventHandler = typeof prop === 'function' && eventHandlerNames[prop]; + if (isEventHandler) { + domProps[prop] = wrapEventHandler(prop); + } + } + } + if (!accessible) { domProps['aria-hidden'] = true; }