Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React Rally: Animated -- React Performance Toolbox

vjeux
September 06, 2015

React Rally: Animated -- React Performance Toolbox

vjeux

September 06, 2015
Tweet

More Decks by vjeux

Other Decks in Programming

Transcript

  1. View Slide

  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.

    View Slide


  3. 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.

    View Slide

  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.

    View Slide

  5. getInitialState() {
    return {left: 0};
    }

    render() {

    return (



    );

    }
    Then, we use inline style in order to render the element.

    View Slide


  6. render() {

    return (



    );

    }

    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.

    View Slide

  7. setState
    Let see the performance characteristics of this approach. We call set
    state on the component.

    View Slide

  8. React is going to call render on this component.

    View Slide

  9. And also calls render on all the components underneath.

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide


  15. Turns out that it’s possible to implement this using a component that
    we call StaticContainer.

    View Slide

  16. render() {

    return (

    shouldUpdate={!this.state.isAnimating}>



    );

    }
    We wrap the with with a prop
    shouldUpdate equal to true when the component is not animating.

    View Slide

  17. class StaticContainer extends React.Component {
    render() {
    return this.props.children;
    }
    shouldComponentUpdate(nextProps) {
    return nextProps.shouldUpdate;
    }
    }
    The implementation is simple: we create a component

    View Slide

  18. class StaticContainer extends React.Component {
    render() {
    return this.props.children;
    }
    shouldComponentUpdate(nextProps) {
    return nextProps.shouldUpdate;
    }
    }
    that just forwards all the children in render

    View Slide

  19. class StaticContainer extends React.Component {
    render() {
    return this.props.children;
    }
    shouldComponentUpdate(nextProps) {
    return nextProps.shouldUpdate;
    }
    }
    and uses shouldUpdate prop to implement
    shouldComponentUpdate

    View Slide

  20. Element Caching
    It turns out that there is another way in React to achieve the same
    result with a technique called Element Caching

    View Slide

  21. ;


    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.

    View Slide

  22. render() {
    this._child = this._child || ;

    return (

    {this._child}

    );

    }
    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.

    View Slide

  23. render() {
    this._child = this._child || ;

    return (

    {this._child}

    );

    }
    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.

    View Slide

  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.

    View Slide

  25. findDOMNode( ).style.left = '10px';
    You can call findDOMNode in order to get the real DOM node and
    then do raw DOM manipulation.

    View Slide

  26. render() {
    return (
    style={{left: this.state.left}}>


    );

    }
    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.

    View Slide

  27. render() {
    return (
    style={{left: this.state.left}}>


    );

    }
    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.

    View Slide

  28. render() {
    return (
    style={{left: this.state.left}}>


    );

    }
    ?????
    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.

    View Slide

  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 :(((

    View Slide

  30. render() {

    return (



    );

    }
    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,

    View Slide

  31. render() {

    return (



    );

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

    View Slide

  32. render() {

    return (



    );

    }
    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.

    View Slide

  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

    View Slide

  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?

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  41. render() {
    return (



    );

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

    View Slide



  42. );

    }
    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.

    View Slide

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

    return ;

    }

    }
    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.

    View Slide

  44. Animated.div = class extends React.Component {
    componentWillReceiveProps(nextProps) {
    nextProps.style.left.onChange(value => {
    React.findDOMNode(this).style.left = value + 'px';
    });

    }
    render() {

    return ;

    }

    }
    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.

    View Slide

  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 :)

    View Slide


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

    render() {

    return ;

    }
    }
    We need to do one last change: 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 element.

    View Slide


  47. 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.

    View Slide

  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.

    View Slide

  49. Thanks!
    Christopher “vjeux” Chedeau

    View Slide