From 19915e455c0396d7a7e4851b18e3b1e4aeb31fee Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 23 Dec 2015 14:52:34 -0800 Subject: [PATCH] IncrementalPreview Preview of new experimental Incremental concept for feedback. I have D2506522 internally that is the source of truth. Video: https://youtu.be/4UNf4-8orQ4 --- Examples/IncrementalPreview.js | 477 +++++++++++++++++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 Examples/IncrementalPreview.js diff --git a/Examples/IncrementalPreview.js b/Examples/IncrementalPreview.js new file mode 100644 index 000000000000..d03b597366c9 --- /dev/null +++ b/Examples/IncrementalPreview.js @@ -0,0 +1,477 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule IncrementalExample + * @flow + */ +'use strict'; + +const React = require('react-native'); +const { + InteractionManager, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} = React; + +const Incremental = require('Incremental'); +const IncrementalGroup = require('IncrementalGroup'); +const IncrementalPresenter = require('IncrementalPresenter'); + +const performanceNow = require('performanceNow'); + +InteractionManager.setDeadline(50); + +const NUM_ITEMS = 20; + +class SlowWidget extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + ctorTimestamp: performanceNow(), + renderTime: 0, + }; + } + render() { + this.state.renderTime === 0 && burnCPU(10); + return ( + + + {`${this.state.renderTime || '?'} ms`} + + + ); + } + componentDidMount() { + const renderTime = performanceNow() - this.state.ctorTimestamp; + this.setState({renderTime}); + } +} + +let imHandle; +function startInteraction() { + imHandle = InteractionManager.createInteractionHandle(); +} +function stopInteraction() { + InteractionManager.clearInteractionHandle(imHandle); +} + +function Block(props: Object) { + const Group = props.stream ? IncrementalGroup : IncrementalPresenter; + return ( + + + + + {props.idx + ': ' + (props.stream ? 'Streaming' : 'Presented')} + + {props.children} + + + + ); +} + +const Row = (props: Object) => ; + +class IncrementalExample extends React.Component { + constructor(props: mixed, context: mixed) { + super(props, context); + this.start = performanceNow(); + } + render(): ReactElement { + return ( + console.log('onDone elapsed: ', performanceNow() - this.start)}> + + + Press and hold on a row to pause rendering. + + {Array(8).fill().map((_, blockIdx) => { + return ( + + {Array(4).fill().map((_, rowIdx) => ( + + {Array(14).fill().map((_, widgetIdx) => ( + + + + ))} + + ))} + + ); + })} + + + ); + } +} +IncrementalExample.title = ''; +IncrementalExample.description = 'Enables incremental rendering of complex components.'; + +function burnCPU(milliseconds) { + const start = performanceNow(); + while (performanceNow() < (start + milliseconds)) {} +} + +var styles = StyleSheet.create({ + scrollView: { + margin: 10, + backgroundColor: 'white', + flex: 1, + }, + headerText: { + fontSize: 20, + margin: 10, + }, + block: { + borderRadius: 6, + borderWidth: 2, + borderColor: '#a52a2a', + padding: 14, + margin: 5, + backgroundColor: 'white', + }, + row: { + flexDirection: 'row', + }, + widgetContainer: { + backgroundColor: '#dddddd', + padding: 2, + margin: 2, + }, + widgetText: { + color: 'black', + fontSize: 4, + }, +}); + +module.exports = IncrementalExample; + + + + + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule Incremental + * @flow + */ +'use strict'; + +const InteractionManager = require('InteractionManager'); +const React = require('React'); + +const DEBUG = false; + +/** + * WARNING: EXPERIMENTAL. Breaking changes will probably happen a lot and will + * not be reliably announced. The whole thing might be deleted, who knows? Use + * at your own risk. + * + * React Native helps make apps smooth by doing all the heavy lifting off the + * main thread, in JavaScript. That works great a lot of the time, except that + * heavy operations like rendering may block the JS thread from responding + * quickly to events like taps, making the app feel sluggish. + * + * `` solves this by slicing up rendering into chunks that are + * spread across multiple event loops. Expensive components can be sliced up + * recursively by wrapping pieces of them and their decendents in + * `` components, and wrapping those in `` + * components, e.g. via `` (similar to an + * `asyncTransactionContainer`) so the group will be presented to the user as + * one unit, rather than popping in sequentially. + * + * `` only affects initial render - setState and other render + * updates are unaffected. + * + * The chunks are rendered sequentially using the `InteractionManager` queue, + * which means that rendering will pause if it's interrupted by an interaction, + * such as an animation or gesture. + * + * Note there is some overhead, so you don't want to slice things up too much. + * a target of 100-200ms of total work per event loop on old/slow devices might + * be a reasonable place to start. + * + * Below is an example that will incrementally render all the parts of row one + * first, then present them together, then repeat the process for row two: + * + * render: function() { + * return ( + * + * {[0,1,2,3,4,5,6,7,8].map((rowIdx) => ( + * + * + * {[0,1,2,3].map((widgetIdx) => ( + * + * + * + * ))} + * + * + * ))} + * + * ); + * }; + */ +export type Props = { + /** + * Called when all the decendents have finished rendering and mounting + * recursively. + */ + onDone?: () => void; + /** + * Tags instances and associated tasks for easier debugging. + */ + name: string; + children: any; +}; +class Incremental extends React.Component { + props: Props; + state: State; + context: Context; + _incrementId: number; + + constructor(props: Props, context: Context) { + super(props, context); + this.state = { + doIncrementalRender: false, + }; + } + + getName(): string { + var ctx = this.context.incrementalGroup || {}; + return ctx.groupId + ':' + this._incrementId + '-' + this.props.name; + } + + componentWillMount() { + var ctx = this.context.incrementalGroup; + if (!ctx) { + return; + } + this._incrementId = ++(ctx.incrementalCount); + InteractionManager.runAfterInteractions({ + name: 'Incremental:' + this.getName(), + gen: () => new Promise(resolve => { + DEBUG && console.log('set doIncrementalRender for ' + this.getName()); + this.setState({doIncrementalRender: true}, resolve); + }), + }).then(() => { + this.props.onDone && this.props.onDone(); + }); + } + + render(): ?ReactElement { + if (!this.context.incrementalGroup || this.state.doIncrementalRender) { + DEBUG && console.log('render ' + this.getName()); + return this.props.children; + } + return null; + } + + componentDidMount() { + if (!this.context.incrementalGroup) { + this.props.onDone && this.props.onDone(); + } + } +} +Incremental.defaultProps = { + name: '', +}; +Incremental.contextTypes = { + incrementalGroup: React.PropTypes.object, +}; + +type State = { + doIncrementalRender: boolean; +}; + +export type Context = { + incrementalGroup: { + groupId: string; + incrementalCount: number; + }; +}; + +module.exports = Incremental; + + + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule IncrementalGroup + * @flow + */ +'use strict'; + +const Incremental = require('Incremental'); +const React = require('React'); + +let _groupCounter = -1; +const DEBUG = false; + +import type {Props, Context} from 'Incremental'; + +/** + * WARNING: EXPERIMENTAL. Breaking changes will probably happen a lot and will + * not be reliably announced. The whole thing might be deleted, who knows? Use + * at your own risk. + * + * `` components must be wrapped in an `` (e.g. + * via ``) in order to provide the incremental group + * context, otherwise they will do nothing. + */ +class IncrementalGroup extends React.Component { + props: Props; + context: Context; + _groupInc: string; + componentWillMount() { + this._groupInc = `g${++_groupCounter}-`; + DEBUG && console.log( + 'create IncrementalGroup with id ' + this.getGroupId() + ); + } + + getGroupId(): string { + const ctx = this.context.incrementalGroup; + const prefix = ctx ? ctx.groupId + ':' : ''; + return prefix + this._groupInc + this.props.name; + } + + getChildContext(): Context { + return { + incrementalGroup: { + groupId: this.getGroupId(), + incrementalCount: -1, + }, + }; + } + + render(): ReactElement { + return ( + + ); + } +} +IncrementalGroup.contextTypes = { + incrementalGroup: React.PropTypes.object, +}; +IncrementalGroup.childContextTypes = IncrementalGroup.contextTypes; + +module.exports = IncrementalGroup; + + + + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule IncrementalPresenter + * @flow + */ +'use strict'; + +const IncrementalGroup = require('IncrementalGroup'); +const React = require('React'); +const View = require('View'); + +import type {Context} from 'Incremental'; + +/** + * WARNING: EXPERIMENTAL. Breaking changes will probably happen a lot and will + * not be reliably announced. The whole thing might be deleted, who knows? Use + * at your own risk. + * + * `` can be used to group sets of `` renders + * such that they are initially invisible and removed from layout until all + * decendents have finished rendering, at which point they are drawn all at once + * so the UI doesn't jump around during the incremental rendering process. + */ +type Props = { + name: string; + onDone: () => void; + style: mixed; + children: any; +} +class IncrementalPresenter extends React.Component { + props: Props; + context: Context; + _isDone: boolean; + constructor(props: Props, context: Context) { + super(props, context); + this._isDone = false; + (this: any).onDone = this.onDone.bind(this); + } + onDone() { + this._isDone = true; + if (this.context.incrementalGroup) { + // Avoid expensive re-renders and use setNativeProps + this.refs.view.setNativeProps( + {style: [this.props.style, {opacity: 1, position: 'relative'}]} + ); + } + this.props.onDone && this.props.onDone(); + } + render() { + if (this.context.incrementalGroup && !this._isDone) { + var style = [this.props.style, {opacity: 0, position: 'absolute'}]; + } else { + var style = this.props.style; + } + return ( + + + + ); + } +} +IncrementalPresenter.propTypes = { + name: React.PropTypes.string, + onDone: React.PropTypes.func, + style: View.propTypes.style, +}; +IncrementalPresenter.contextTypes = { + incrementalGroup: React.PropTypes.object, +}; + +module.exports = IncrementalPresenter;