mirror of
https://github.com/status-im/react-native.git
synced 2025-01-19 05:51:01 +00:00
838d8d4094
Summary: Incremental rendering is a tradeoff between throughput and responsiveness because it yields. When we have plenty of buffer (say 50% of the target), we render incrementally to keep the app responsive. If we are dangerously low on buffer (say below 25%) we always disable incremental to try to catch up as fast as possible. In between, we only disable incremental while actively scrolling since it's unlikely the user will try to press a button while scrolling. This also optimizes some things then incremental is switching back and forth. I played around with making the render window itself adaptive, but it seems pretty futile to predict - once the user decides to scroll quickly in some direction, it's pretty much too late and increasing the render window size won't help because we're already limited by the render throughput at that point. Reviewed By: ericvicenti Differential Revision: D3250916 fbshipit-source-id: 930d418522a3bf3e20083e60f6eb6f891497a2b8
187 lines
5.9 KiB
JavaScript
187 lines
5.9 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 infoLog = require('infoLog');
|
|
|
|
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;
|
|
};
|
|
type DefaultProps = {
|
|
name: string,
|
|
};
|
|
type State = {
|
|
doIncrementalRender: boolean,
|
|
};
|
|
class Incremental extends React.Component<DefaultProps, Props, State> {
|
|
props: Props;
|
|
state: State;
|
|
context: Context;
|
|
_incrementId: number;
|
|
_mounted: boolean;
|
|
_rendered: boolean;
|
|
|
|
static defaultProps = {
|
|
name: '',
|
|
};
|
|
|
|
static contextTypes = {
|
|
incrementalGroup: React.PropTypes.object,
|
|
incrementalGroupEnabled: React.PropTypes.bool,
|
|
};
|
|
|
|
constructor(props: Props, context: Context) {
|
|
super(props, context);
|
|
this._mounted = false;
|
|
this.state = {
|
|
doIncrementalRender: false,
|
|
};
|
|
}
|
|
|
|
getName(): string {
|
|
const ctx = this.context.incrementalGroup || {};
|
|
return ctx.groupId + ':' + this._incrementId + '-' + this.props.name;
|
|
}
|
|
|
|
componentWillMount() {
|
|
const ctx = this.context.incrementalGroup;
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
this._incrementId = ++(ctx.incrementalCount);
|
|
InteractionManager.runAfterInteractions({
|
|
name: 'Incremental:' + this.getName(),
|
|
gen: () => new Promise(resolve => {
|
|
if (!this._mounted || this._rendered) {
|
|
resolve();
|
|
return;
|
|
}
|
|
DEBUG && infoLog('set doIncrementalRender for ' + this.getName());
|
|
this.setState({doIncrementalRender: true}, resolve);
|
|
}),
|
|
}).then(() => {
|
|
DEBUG && infoLog('call onDone for ' + this.getName());
|
|
this._mounted && this.props.onDone && this.props.onDone();
|
|
}).catch((ex) => {
|
|
ex.message = `Incremental render failed for ${this.getName()}: ${ex.message}`;
|
|
throw ex;
|
|
}).done();
|
|
}
|
|
|
|
render(): ?ReactElement {
|
|
if (this._rendered || // Make sure that once we render once, we stay rendered even if incrementalGroupEnabled gets flipped.
|
|
!this.context.incrementalGroupEnabled ||
|
|
this.state.doIncrementalRender) {
|
|
DEBUG && infoLog('render ' + this.getName());
|
|
this._rendered = true;
|
|
return this.props.children;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
componentDidMount() {
|
|
this._mounted = true;
|
|
if (!this.context.incrementalGroup) {
|
|
this.props.onDone && this.props.onDone();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this._mounted = false;
|
|
}
|
|
}
|
|
|
|
export type Context = {
|
|
incrementalGroupEnabled: boolean;
|
|
incrementalGroup: ?{
|
|
groupId: string;
|
|
incrementalCount: number;
|
|
};
|
|
};
|
|
|
|
module.exports = Incremental;
|