Spencer Ahrens f21da3aa31 <Incremental> for incremental rendering
Summary:Everything wrapped in `<Incremental>` is rendered sequentially via `InteractionManager`.
The `onDone` callback is called when all descendent incremental components have
finished rendering, used by `<IncrementalPresenter>` to make the story visible all at once
instead of the parts popping in randomly.

This includes an example that demonstrates streaming rendering and the use of
`<IncrementalPresenter>`.  Pressing down pauses rendering and you can see the
`TouchableOpacity` animation runs smoothly.  Video:

https://youtu.be/4UNf4-8orQ4

Ideally this will be baked into React Core at some point, but according to jordwalke that's
going to require a major refactoring and take a long time, so going with this for now.

Reviewed By: ericvicenti

Differential Revision: D2506522

fb-gh-sync-id: 5969bf248de10d38b0ac22f34d7d49bf1b3ac4b6
shipit-source-id: 5969bf248de10d38b0ac22f34d7d49bf1b3ac4b6
2016-03-10 08:14:23 -08:00

172 lines
5.4 KiB
JavaScript

/**
* 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.
*
* `<Incremental>` 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
* `<Incremental>` components. `<IncrementalGroup>` can be used to make sure
* everything in the group is rendered recursively before calling `onDone` and
* moving on to another sibling group (e.g. render one row at a time, even if
* rendering the top level row component produces more `<Incremental>` chunks).
* `<IncrementalPresenter>` is a type of `<IncrementalGroup>` that keeps it's
* children invisible and out of the layout tree until all rendering completes
* recursively. This means the group will be presented to the user as one unit,
* rather than pieces popping in sequentially.
*
* `<Incremental>` 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, and
* so on:
*
* render: function() {
* return (
* <ScrollView>
* {Array(10).fill().map((rowIdx) => (
* <IncrementalPresenter key={rowIdx}>
* <Row>
* {Array(20).fill().map((widgetIdx) => (
* <Incremental key={widgetIdx}>
* <SlowWidget />
* </Incremental>
* ))}
* </Row>
* </IncrementalPresenter>
* ))}
* </ScrollView>
* );
* };
*
* If SlowWidget takes 30ms to render, then without `Incremental`, this would
* block the JS thread for at least `10 * 20 * 30ms = 6000ms`, but with
* `Incremental` it will probably not block for more than 50-100ms at a time,
* allowing user interactions to take place which might even unmount this
* component, saving us from ever doing the remaining rendering work.
*/
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;
disabled: boolean;
};
class Incremental extends React.Component {
props: Props;
state: {
doIncrementalRender: boolean;
};
context: Context;
_incrementId: number;
_mounted: boolean;
constructor(props: Props, context: Context) {
super(props, context);
this._mounted = false;
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 => {
if (!this._mounted) {
resolve();
return;
}
DEBUG && console.log('set doIncrementalRender for ' + this.getName());
this.setState({doIncrementalRender: true}, resolve);
}),
}).then(() => {
this._mounted && this.props.onDone && this.props.onDone();
});
}
render(): ?ReactElement {
if (this.props.disabled || !this.context.incrementalGroupEnabled || this.state.doIncrementalRender) {
DEBUG && console.log('render ' + this.getName());
return this.props.children;
}
return null;
}
componentDidMount() {
this._mounted = true;
if (!this.context.incrementalGroup) {
this.props.onDone && this.props.onDone();
}
}
componentWillUnmount() {
this._mounted = false;
}
}
Incremental.defaultProps = {
name: '',
};
Incremental.contextTypes = {
incrementalGroup: React.PropTypes.object,
incrementalGroupEnabled: React.PropTypes.bool,
};
export type Context = {
incrementalGroupEnabled: boolean;
incrementalGroup: ?{
groupId: string;
incrementalCount: number;
};
};
module.exports = Incremental;