React Rally: Animated -- React Performance Toolbox

48a313e2ad9f47036b3a4d073cef3e54?s=47 vjeux
September 06, 2015

React Rally: Animated -- React Performance Toolbox

48a313e2ad9f47036b3a4d073cef3e54?s=128

vjeux

September 06, 2015
Tweet

Transcript

  1. 1.
  2. 2.

    Hey everyone, my name is Christopher Chedeau also know as

    vjeux on the internet, I’m working on React Native. The goal of this project is to let you write high quality mobile applications that are indistinguishable from native apps. One of the big challenge when using React on mobile was animations. Unlike on desktop, animations are everywhere and you have a hard 16ms time constraint on a slower device. In this talk, I’m going to explain all the approaches we’ve tried to get high performance animations in React Native.
  3. 3.

    <StaticContainer> Element Caching ↑ ↑ ↓ ↓ ← → ←

    → B A Raw DOM Mutations shouldComponentUpdate I’m going to introduce you five techniques that you can add to your toolbox to optimize your React application. I’ll introduce the “secret” one that we use in the React Native Animated library.
  4. 4.

    getInitialState() { return {left: 0}; } We’re going to focus

    on one simple animation for the purpose of this talk: sliding an element in the screen. If you follow the React tutorial, you’re going to keep the animation progress in a state variable.
  5. 5.

    getInitialState() { return {left: 0}; }
 render() {
 return (

    <div style={{left: this.state.left}}> <Child /> </div> );
 } Then, we use inline style in order to render the element.
  6. 6.

    
 render() {
 return ( <div style={{left: this.state.left}}> <Child />

    </div> );
 } 
 onChange(value) {
 this.setState({left: value});
 } And finally, on requestAnimationFrame, we call onChange with the new value and use setState to re-render the component.
  7. 10.

    This may sound crazy to have the default being to

    re-render the entire subtree. But it’s actually a very good one. In order to improve the performance of a system, you need to add more constraints and therefore put a burden on the developer. It turns out that most of your application is usually not performance sensitive. In cases where you run into a performance issue, then it’s time to make those trade-offs.
  8. 11.

    shouldComponentUpdate The recommended way to optimize this example is to

    implement shouldComponentUpdate on the children. This is a way to tell React that we know that nothing changed.
  9. 12.

    You Person A Person B While it solves most performance

    issues, it is not straightforward to implement in practice. The reason is social: the children are often not owned by the same person. Implementing shouldComponentUpdate requires to refactor the component to ensure that it uses immutable data structures and avoid bound functions. In itself this is not very hard, but now you need to coordinate with 2 other people to animate that component.
  10. 13.

    You Person A Person B This breaks one of the

    key property of React: isolation. If you want to animate a component, you shouldn’t have to touch other components.
  11. 14.

    You Person A Person B What we really want is

    to move that decision of when to re-render to the parent: do not re-render anything while animating. This way we can preserve isolation.
  12. 15.
  13. 16.

    render() {
 return ( <div style={{left: this.state.left}}> <StaticContainer shouldUpdate={!this.state.isAnimating}> <ExpensiveChild

    /> </StaticContainer> </div> );
 } We wrap the <ExpensiveChild /> with <StaticContainer> with a prop shouldUpdate equal to true when the component is not animating.
  14. 17.

    class StaticContainer extends React.Component { render() { return this.props.children; }

    shouldComponentUpdate(nextProps) { return nextProps.shouldUpdate; } } The implementation is simple: we create a component
  15. 18.

    class StaticContainer extends React.Component { render() { return this.props.children; }

    shouldComponentUpdate(nextProps) { return nextProps.shouldUpdate; } } that just forwards all the children in render
  16. 19.

    class StaticContainer extends React.Component { render() { return this.props.children; }

    shouldComponentUpdate(nextProps) { return nextProps.shouldUpdate; } } and uses shouldUpdate prop to implement shouldComponentUpdate
  17. 20.

    Element Caching It turns out that there is another way

    in React to achieve the same result with a technique called Element Caching
  18. 21.

    <ExpensiveChild />;
 
 Element To understand the technique, we first

    need to understand what a React Element is. It’s the thing you get when you use a JSX tag. You can also get one using React.createElement() if you are not using JSX.
  19. 22.

    render() { this._child = this._child || <ExpensiveChild />;
 return (

    <div style={{left: this.state.left}}> {this._child} </div> );
 } Element During the diff algorithm, when React compares two elements (before vs after), if those two elements are === the same, then React is going to bail and not re-render it. Knowing this, we can cache the element inside of an instance variable and always return the same one.
  20. 23.

    render() { this._child = this._child || <ExpensiveChild />;
 return (

    <div style={{left: this.state.left}}> {this._child} </div> );
 } Element Race Condition With those two techniques, we’ve been able to solve the performance issues of our application, unfortunately they are not a silver bullet (otherwise we would have made them the default!). In this case, we are now susceptible to race conditions. If an update came in during the animation, we’re going to skip it. If we do not re- render with the new value at the end of the animation, then we’re going to show stale data. And worse, if sometimes in the future the components gets re- rendered, then the new value is going to flash in for no obvious reason.
  21. 24.

    Raw DOM Mutations If we go back to the initial

    problem, we just want to update a single attribute on a single node. We went to great length to work around the React diffing algorithm but really, we don’t want React to re- render anything or diff anything. We just want to set the attribute. Turns out that React gives you a way to implement that.
  22. 25.

    findDOMNode( ).style.left = '10px'; You can call findDOMNode in order

    to get the real DOM node and then do raw DOM manipulation.
  23. 26.

    render() { return ( <div style={{left: this.state.left}}> <ExpensiveChild /> </div>

    );
 } onUpdate(value) { React.findDOMNode(this).style.left = value + 'px'; } This is the code that makes it work, nothing difficult. It gives us the most performant way to solve the problem at hand: we just set the value when it updates.
  24. 27.

    render() { return ( <div style={{left: this.state.left}}> <ExpensiveChild /> </div>

    );
 } onUpdate(value) { React.findDOMNode(this).style.left = value + 'px'; } Unfortunately, this is no free lunch either. You’re now facing the $1 billion mistake: null references. If the component is unmounted while the animation is still running, then you’re going to try to set the value on a DOM node that doesn’t exist anymore.
  25. 28.

    render() { return ( <div style={{left: this.state.left}}> <ExpensiveChild /> </div>

    );
 } ????? And, you’re not exempt from the race conditions either. In this case, React normal rendering process is going to have one value for the attribute and when you use raw DOM manipulation you’re going to have another one. If the two values are different, then whoever gets in last wins.
  26. 29.

    ↑ ↑ ↓ ↓ ← → ← → B A

    So far, we’ve seen three techniques that let us get the performance we desired, but the trade-off is that we lost some of the strong guarantees that React give us. I love React because it eliminated a huge amount of bugs, but those surfaced again in React Native on code that implemented animations :(((
  27. 30.

    render() {
 return ( <div style={{left: this.state.left}}> <Child /> </div>

    );
 } I wanted to figure out if there was a way to get the performance of raw DOM manipulation but with the safety guarantees of React. If we go back to the initial example,
  28. 31.

    render() {
 return ( <div style={{left: this.state.left}}> <Child /> </div>

    );
 } then as a developer we explicitly write that [div style left] has the value [this.state.left].
  29. 32.

    render() {
 return ( <div style={{left: this.state.left}}> <Child /> </div>

    );
 } onChange(value) {
 this.setState({left: value});
 } React.findDOMNode(this).style.left = value + 'px'; So, there’s no reason why React cannot be smart enough to trigger a raw DOM mutation whenever we change the left value.
  30. 33.

    Before we go to the next slide, I want you

    to take a moment and forget everything you know about React Keep an open mind
  31. 34.

    Data Binding This technique actually has a name: Data Binding.

    Now you probably wonder why Pete Hunt went on stage and told everyone that we chose *not* to use Data Binding for React. Was he wrong? Am I delusional?
  32. 35.

    Memory Initialization cost O(1) updates per binding ( Data Binding

    To answer those questions, let’s see what are the characteristics of Data Binding. We have to pay some memory and initialization time for every single binding. But as a result we get ultra-fast O(1) updates as we know for each value all the places where it is being used.
  33. 36.

    Model >> View User Interface Before React, Data Binding was

    used to drive the entire UI. It turns out that most of the time, your model is much bigger than what you render. You probably have data in cache, implement some sort of pagination… So having to pay the cost of all the data you have in cache when you only display a handful is wasteful.
  34. 37.

    Most values do not change When they change, in big

    blocks User Interface The second realization is that the most frequent style of updates is to tear down an entire subview and replace it by another one. You have in places just small updates that do happen, but with React, having this coarse update mechanism is usually fast enough.
  35. 38.

    Startup time matters a lot User Interface The trade-off that

    Data Binding makes is to have more work done at startup in order to make updates faster. But, for a website like Facebook, startup time is absolutely critical. So React model is better suited.
  36. 39.

    Only a few attributes Animations Now, if we take the

    same constraints and apply them to Animations, then we get a very different story. Most of the time, you are only animating a few elements at a time and just changing a few attributes such as left or opacity. Creating a dozen of bindings is not very expensive.
  37. 40.

    Frame performance >>>>> Startup time Animations In the case of

    animations, we have a —very hard— deadline, we need to squeeze every bit of performance we can and Data Binding gives us some very nice properties in that regard.
  38. 41.

    render() { return ( <Animated.div style={{left: this.state.left}}> <ExpensiveChild /> </Animated.div>

    );
 } So, let’s see how we can implement Data Binding in React. The first thing we need to do is to change <div> to <Animated.div>. The default <div> doesn’t know about bindings so we need to write one that does.
  39. 42.

    <ExpensiveChild /> </Animated.div> );
 } getInitialState() { return {left: new

    Animated.Value(0)}; } onUpdate(value) { this.state.left.setValue(value); } In the same vein, we cannot use just a regular number, we need to wrap it into an object that we can listen for changes. In React Native, we call it new Animated.Value and you can call setValue on it. I have hopes that we can add an optimization in Babel that would allow us to do that as an optimization pass for regular React code, but it’s very much a theory at this point.
  40. 43.

    Animated.div = class extends React.Component { render() {
 return <div

    {...this.props} />;
 }
 } Okay, so now let’s implement the special div that knows about binding. The first thing we need to do is to create a wrapper component that just forwards all the props. This can be used as if it was a normal div.
  41. 44.

    Animated.div = class extends React.Component { componentWillReceiveProps(nextProps) { nextProps.style.left.onChange(value =>

    { React.findDOMNode(this).style.left = value + 'px'; });
 } render() {
 return <div {...this.props} />;
 }
 } Now, every time we receive some props, we want to add our binding: whenever the value changes, we want to trigger the raw DOM mutation.
  42. 45.

    Animated.div = class extends React.Component { componentWillUnmount() {
 this.props.style.left.removeAllListeners();
 }

    componentWillReceiveProps(nextProps) {
 this.props.style.left.removeAllListeners(); nextProps.left.onChange(value => { React.findDOMNode(this).style.left = value + 'px'; }); } But, if we were to implement it as is, it would have crazy memory leaks: we never clean up bindings, so every time we render we allocate new bindings! Fortunately, React lifecycle methods have us covered. We can clean up in componentWillUnmount and before receiving new props :)
  43. 46.

    
 this._props = React.addons.update( nextProps, {style: {left: {$set: nextProps.left.getValue()}}}, );

    }
 render() {
 return <div {...this._props} />;
 } } We need to do one last change: <div> doesn’t know about bindings. Instead, we want to replace the binding with the real value (just a number here) before sending it to the <div> element.
  44. 47.

    <StaticContainer> Element Caching Data Binding Raw DOM Mutations shouldComponentUpdate If

    you have some performance issues with your React app, I highly encourage to evaluate those techniques and see if the trade-offs make sense.
  45. 48.

    React by defaults it provides a reasonably good way to

    update the DOM after some change. But what is mind blowing to me is the fact that it provides the hooks needed to implement different strategies that are better suited for your use case. Not only that, but you can encapsulate those within a component and use it like a regular component. No one has to know that your component uses data binding in order to drive animations instead of the diff algorithm.